From f4c086d26d4aec32ce94d9eea1d38779411b6c56 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 12:31:41 -0700 Subject: [PATCH 01/24] quest(m1): plan the hidden floor below a resolved relay copy A relay folds an absolute floor into an "oldest" reader's demand after that reader resolved its copy, so the lower floor never reaches upstream. Found in #5931; the fix lands after #5929's cached-group re-offer. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/resolved-demand.md | 63 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 quest/m1/resolved-demand.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 18f4bd1d617..a661e0c8911 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -110,6 +110,7 @@ lands before the cut. - [Timed-out requests clean up](/quest/m1/legacy-request-timeout-cleanup.md) - on drafts 14-16, Rust and JS send PUBLISH_NAMESPACE_DONE or UNSUBSCRIBE_NAMESPACE for a timed-out request before its local reset - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide - [Resume reorder](/quest/m1/resume-reorder.md) - a group whose stream header arrives after its successor's still resumes on failover; only a dropped or long-gone group fails +- [A floor below a resolved copy reaches upstream](/quest/m1/resolved-demand.md) - a relay fetches groups a reader asks for below where an "oldest" reader already resolved its copy - [Received timestamps are media time](/quest/m1/lite-time-provenance.md) - a received timed track's timestamps are media time, so the rewind check can run across hops - [WebGPU on Safari](/quest/m1/webgpu-safari.md) - the WebGPU renderer is verified on Safari 26 for macOS and iOS - [HLS export drains after End](/quest/m1/hls-export-end-drain.md) - a cleanly ended broadcast still exports its retained segments diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md new file mode 100644 index 00000000000..306a06b39a3 --- /dev/null +++ b/quest/m1/resolved-demand.md @@ -0,0 +1,63 @@ +# [M] A floor below a resolved copy reaches upstream + +## Goal + +A relay fetches upstream the groups a new reader asks for with an absolute +floor below where its copy already resolved. Today it never does: once a +reader asking for the oldest group (`Range::oldest`, or a relative start) +holds a copy, the demand union folds every other range into "oldest" +(`rs/moq-net/src/model/track/demand/ranges.rs`), so a later reader's floor at +group 5, under a copy resolved at group 10, sends no SUBSCRIBE_UPDATE. +Upstream already moved its cursor past 5 and never offers it, even while it +still caches 5 through 9 and the new reader's max age covers them. The +reader silently starts later than it asked, and only through a warm relay. + +An archive resuming at its last stored frame through a relay that a viewer +already warmed hits this, as does any absolute range start. + +## Plan + +Found while building SUBSCRIBE_ALL +([moq-dev/moq#5931](https://github.com/moq-dev/moq/pull/5931)), which +reproduced it with a throwaway test and fixed it only for listed tracks +(`listed_demand` in `f074d09e7`): once a subscription's start resolves, its +demand counts from that start rather than as "oldest", so a lower floor +changes the union and goes upstream. Generalize that to every subscription, +or find a narrower shape with the same effect. + +Lands after [moq-dev/moq#5929](https://github.com/moq-dev/moq/pull/5929), +which makes a lowered floor re-offer cached groups at the publisher (decided +2026-10-19). This quest gets the lower floor to the upstream hop; #5929 makes +that hop serve it. Each is a no-op for this bug without the other. #5929 also +moves relative-start resolution into the model (`track::Subscriber::floor()`), +which is the natural place to learn that a start resolved. + +Watch for: + +- **The start is not a floor.** The lite draft's SUBSCRIBE_OK says a group + below the resolved start that becomes available later is still delivered + within max age. Turning every resolved reader into an absolute floor + upstream would drop such late groups. Keep that property: one option is to + count a resolved start only when comparing against another reader's floor, + and send a floor upstream only when one lands below the copy's start. +- **Every relay changes.** This alters the upstream SUBSCRIBE_UPDATE stream + of every relayed track. Make sure a reader resolving does not, by itself, + send an update per subscriber; extend `track_subscriber_churn` in + `rs/moq-net/benches/track.rs` with resolving readers, swept over subscriber + count. +- Check whether `js/net` aggregates demand the same way and needs the same + fix. + +Test on mock time through a relay: a reader with an absolute floor below a +copy an "oldest" reader resolved gets the groups upstream still caches (fails +on `main`); the same through a cold relay gives the same answer; a late group +below a resolved start is still delivered within max age; and a reader +resolving alone sends no SUBSCRIBE_UPDATE. + +Public API: none. Wire: no encoding change; a relay now sends a lower +SUBSCRIBE_UPDATE floor upstream in the case above. + +## Related + +- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - #5929 lands its serving half, including the cached-group re-offer this relies on +- [Archive producer](/quest/m1/archive/producer.md) - its resume floor crosses relays that viewers may have warmed From f32585e3a20a52148e57b30dcd301e354bd1d2e9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 12:40:13 -0700 Subject: [PATCH 02/24] quest(m1): resolved demand is compare-only A resolved oldest start counts as S when aggregating floors but never filters delivery, so late groups below it still arrive. Decided by the maintainer 2026-10-19. Co-Authored-By: Claude Opus 5.5 --- quest/m1/resolved-demand.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 306a06b39a3..18bd9d89a87 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -20,10 +20,22 @@ already warmed hits this, as does any absolute range start. Found while building SUBSCRIBE_ALL ([moq-dev/moq#5931](https://github.com/moq-dev/moq/pull/5931)), which reproduced it with a throwaway test and fixed it only for listed tracks -(`listed_demand` in `f074d09e7`): once a subscription's start resolves, its -demand counts from that start rather than as "oldest", so a lower floor -changes the union and goes upstream. Generalize that to every subscription, -or find a narrower shape with the same effect. +(`listed_demand` in `f074d09e7`) by turning a listed track's demand into an +absolute floor at its resolved start. + +Decided by the maintainer on 2026-10-19, judged on behavior: compare-only. A +resolved "oldest" start is information about the copy, not a filter. + +- **Aggregation.** An "oldest" reader that resolved at S counts as S, so the + copy's demand is min(S, every other reader's floor), and a newcomer at + F < S sends F upstream. +- **Delivery.** Nothing filters the existing reader, so a group below S that + arrives late still reaches it within its max age. + +Rejected: #5931's absolute floor at the resolved start, which drops those +late groups. The lite draft's SUBSCRIBE_OK already says its group "is not a +new floor" and that a later group below it is still delivered, which matches +this, so the draft does not change. Lands after [moq-dev/moq#5929](https://github.com/moq-dev/moq/pull/5929), which makes a lowered floor re-offer cached groups at the publisher (decided @@ -34,12 +46,6 @@ which is the natural place to learn that a start resolved. Watch for: -- **The start is not a floor.** The lite draft's SUBSCRIBE_OK says a group - below the resolved start that becomes available later is still delivered - within max age. Turning every resolved reader into an absolute floor - upstream would drop such late groups. Keep that property: one option is to - count a resolved start only when comparing against another reader's floor, - and send a floor upstream only when one lands below the copy's start. - **Every relay changes.** This alters the upstream SUBSCRIBE_UPDATE stream of every relayed track. Make sure a reader resolving does not, by itself, send an update per subscriber; extend `track_subscriber_churn` in From 4537e3c2c0c66463bc10fede43441124371df86f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 12:55:13 -0700 Subject: [PATCH 03/24] quest(m1): resolved demand covers js/net combineSubscriptions in js/net clears the aggregate start whenever any subscriber has no floor, so the warm-relay case needs the same fix and a JS regression test. Co-Authored-By: Claude Opus 5.5 --- quest/m1/resolved-demand.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 18bd9d89a87..d011521f440 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -51,14 +51,18 @@ Watch for: send an update per subscriber; extend `track_subscriber_churn` in `rs/moq-net/benches/track.rs` with resolving readers, swept over subscriber count. -- Check whether `js/net` aggregates demand the same way and needs the same - fix. +- **`js/net` has the same bug and is in scope.** `combineSubscriptions` in + `js/net/src/track.ts` clears the aggregate start whenever any subscriber + has no floor, while `hooks.startTrack` records the copy's resolved start, so + a warm JS relay suppresses a lower absolute floor the same way. Fix it with + the same rule and land a JS regression test beside the Rust one. Test on mock time through a relay: a reader with an absolute floor below a copy an "oldest" reader resolved gets the groups upstream still caches (fails on `main`); the same through a cold relay gives the same answer; a late group below a resolved start is still delivered within max age; and a reader -resolving alone sends no SUBSCRIBE_UPDATE. +resolving alone sends no SUBSCRIBE_UPDATE. Cover the warm-relay case in +`js/net` too (fails on `main`). Public API: none. Wire: no encoding change; a relay now sends a lower SUBSCRIBE_UPDATE floor upstream in the case above. From cd54887e9e21b97dc4154be9fcdf81faf7a09d7f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 13:17:18 -0700 Subject: [PATCH 04/24] quest(m1): a widened max age re-offers cached groups The maintainer chose re-offer on a widened max age with no wire change. The aggregate stays the union of subscriber ranges, so a no-floor reader keeps the upstream floor off; track.rs confirms a widened budget does not re-offer committed skips today. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 +- quest/m1/resolved-demand.md | 118 +++++++++++++++++++++--------------- 2 files changed, 69 insertions(+), 51 deletions(-) diff --git a/quest/m1/README.md b/quest/m1/README.md index 571725936ba..65ff6023a46 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -109,7 +109,7 @@ lands before the cut. - [Timed-out requests clean up](/quest/m1/legacy-request-timeout-cleanup.md) - on drafts 14-16, Rust and JS send PUBLISH_NAMESPACE_DONE or UNSUBSCRIBE_NAMESPACE for a timed-out request before its local reset - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide - [Resume reorder](/quest/m1/resume-reorder.md) - a group whose stream header arrives after its successor's still resumes on failover; only a dropped or long-gone group fails -- [A floor below a resolved copy reaches upstream](/quest/m1/resolved-demand.md) - a relay fetches groups a reader asks for below where an "oldest" reader already resolved its copy +- [A widened max age re-offers cached groups](/quest/m1/resolved-demand.md) - a warm relay serves a reader with a wider max age the cached groups a cold relay would, with no wire change - [Received timestamps are media time](/quest/m1/lite-time-provenance.md) - a received timed track's timestamps are media time, so the rewind check can run across hops - [WebGPU on Safari](/quest/m1/webgpu-safari.md) - the WebGPU renderer is verified on Safari 26 for macOS and iOS - [HLS export drains after End](/quest/m1/hls-export-end-drain.md) - a cleanly ended broadcast still exports its retained segments diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index d011521f440..6c931bac5e8 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -1,73 +1,91 @@ -# [M] A floor below a resolved copy reaches upstream +# [M] A widened max age re-offers cached groups ## Goal -A relay fetches upstream the groups a new reader asks for with an absolute -floor below where its copy already resolved. Today it never does: once a -reader asking for the oldest group (`Range::oldest`, or a relative start) -holds a copy, the demand union folds every other range into "oldest" -(`rs/moq-net/src/model/track/demand/ranges.rs`), so a later reader's floor at -group 5, under a copy resolved at group 10, sends no SUBSCRIBE_UPDATE. -Upstream already moved its cursor past 5 and never offers it, even while it -still caches 5 through 9 and the new reader's max age covers them. The +A warm relay serves a new reader the same groups a cold one would. Today it +does not: once a reader asking for the oldest group (`Range::oldest`) holds +a copy, its upstream subscription resolved where that reader's max age put +it, say group 10. A newcomer whose wider max age covers groups 5 through 9, +with a floor at 5 or with no floor at all, gets nothing below 10. The relay +does send the wider max age upstream in SUBSCRIBE_UPDATE, but the upstream +cursor never re-offers the cached groups it already skipped as stale. The reader silently starts later than it asked, and only through a warm relay. An archive resuming at its last stored frame through a relay that a viewer -already warmed hits this, as does any absolute range start. +already warmed hits this, as does any reader with a wider max age than the +copy's first one. ## Plan Found while building SUBSCRIBE_ALL ([moq-dev/moq#5931](https://github.com/moq-dev/moq/pull/5931)), which -reproduced it with a throwaway test and fixed it only for listed tracks -(`listed_demand` in `f074d09e7`) by turning a listed track's demand into an -absolute floor at its resolved start. +patched it for listed tracks only by sending the resolved start as an +absolute floor (`listed_demand` in `f074d09e7`). -Decided by the maintainer on 2026-10-19, judged on behavior: compare-only. A -resolved "oldest" start is information about the copy, not a filter. +Decided by the maintainer on 2026-10-19: re-offer when the max age widens, +with no wire change. -- **Aggregation.** An "oldest" reader that resolved at S counts as S, so the - copy's demand is min(S, every other reader's floor), and a newcomer at - F < S sends F upstream. -- **Delivery.** Nothing filters the existing reader, so a group below S that - arrives late still reaches it within its max age. - -Rejected: #5931's absolute floor at the resolved start, which drops those -late groups. The lite draft's SUBSCRIBE_OK already says its group "is not a -new floor" and that a later group below it is still delivered, which matches -this, so the draft does not change. - -Lands after [moq-dev/moq#5929](https://github.com/moq-dev/moq/pull/5929), -which makes a lowered floor re-offer cached groups at the publisher (decided -2026-10-19). This quest gets the lower floor to the upstream hop; #5929 makes -that hop serve it. Each is a no-op for this bug without the other. #5929 also -moves relative-start resolution into the model (`track::Subscriber::floor()`), -which is the natural place to learn that a start resolved. +- **The aggregate is the union of subscriber ranges**, as + [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) already decided + ("Merging subscribers is the union of their ranges"). On a version with a + single floor, the union of a no-floor reader and a floor F is no floor, so + the relay sends no floor upstream and filters F locally, which the Rust and + JS aggregates already do. A floor on the wire + is a filter (the publisher "MUST NOT deliver an older one"), so sending one + would drop a late group the no-floor reader is still owed. +- **Why max age, not the floor.** An "oldest" reader resolves at S only + because the groups below S were stale under its max age, or missing. Age is + measured against the newest group, so it only grows. A newcomer is owed a + group below S only if its max age is wider, and a wider max age already + changes the aggregate (the max of every reader's) and goes upstream in + SUBSCRIBE_UPDATE. The missing piece is the publisher acting on it. +- **Today a widened max age does not re-offer.** Confirmed in + `rs/moq-net/src/model/track.rs`: the ordered cursor (`poll_next_group`) + holds a stale conviction in `seek_pending` and walks it back if the budget + widens, but only until it delivers a later group, which commits the skip + and moves `next_sequence` past it for good. The arrival-order path drops + the gaps past budget from `Delivered` (`forget_past_budget`) and never + revisits them. Only an uncommitted conviction can come back. +- **The fix.** When a subscriber's max age widens, re-offer the cached groups + at or above its floor that its cursor gave up as stale and that the new + budget covers, lowest first. This extends the re-offer + [moq-dev/moq#5929](https://github.com/moq-dev/moq/pull/5929) adds for a + widened range (a group outside the ranges is parked and re-offered when they + widen); land after it and reuse that path rather than a second one. Only + groups this cursor never delivered come back. +- **Cost, accepted.** Groups below a newcomer's floor that the wider budget + covers also come upstream, and the relay drops them locally. Bounded by the + max age and the cache. +- Rejected: sending min(S, floors) as the wire floor, which filters late + groups for the no-floor reader; a second upstream SUBSCRIBE bounded to the + gap, which costs a slot, puts two pulls into one copy, and misses the + no-floor case; and a new wire hint. Watch for: -- **Every relay changes.** This alters the upstream SUBSCRIBE_UPDATE stream - of every relayed track. Make sure a reader resolving does not, by itself, - send an update per subscriber; extend `track_subscriber_churn` in - `rs/moq-net/benches/track.rs` with resolving readers, swept over subscriber - count. +- **Every relay changes.** A widening now costs the publisher a scan of its + cached groups below the cursor. Keep it proportional to the groups + re-offered, not the cache, and make sure a reader joining with the same max + age causes no scan and no SUBSCRIBE_UPDATE. Extend `track_subscriber_churn` + in `rs/moq-net/benches/track.rs` with widening readers, swept over + subscriber count and cached groups. - **`js/net` has the same bug and is in scope.** `combineSubscriptions` in - `js/net/src/track.ts` clears the aggregate start whenever any subscriber - has no floor, while `hooks.startTrack` records the copy's resolved start, so - a warm JS relay suppresses a lower absolute floor the same way. Fix it with - the same rule and land a JS regression test beside the Rust one. + `js/net/src/track.ts` already sends no floor when any subscriber has none, + and its publisher needs the same re-offer on a widened max age. Land a JS + regression test beside the Rust one. -Test on mock time through a relay: a reader with an absolute floor below a -copy an "oldest" reader resolved gets the groups upstream still caches (fails -on `main`); the same through a cold relay gives the same answer; a late group -below a resolved start is still delivered within max age; and a reader -resolving alone sends no SUBSCRIBE_UPDATE. Cover the warm-relay case in -`js/net` too (fails on `main`). +Test on mock time through a relay: a newcomer with a wider max age, with a +floor below the copy's start and with none, gets the groups upstream still +caches (fails on `main`); the same through a cold relay gives the same +answer; a late group below the copy's start still reaches the no-floor +reader; a reader joining with the same max age sends no SUBSCRIBE_UPDATE; and +a re-offered group is never delivered twice to one subscriber. Cover the +warm-relay case in `js/net` too. -Public API: none. Wire: no encoding change; a relay now sends a lower -SUBSCRIBE_UPDATE floor upstream in the case above. +Public API: none. Wire: no encoding change; a publisher now re-offers +cached groups when a SUBSCRIBE_UPDATE widens the max age. ## Related -- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - #5929 lands its serving half, including the cached-group re-offer this relies on +- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - #5929 lands its serving half, including the re-offer this extends - [Archive producer](/quest/m1/archive/producer.md) - its resume floor crosses relays that viewers may have warmed From 299abbd12bd3cb4f24bc290d2e33f51f561b54ff Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 13:24:36 -0700 Subject: [PATCH 05/24] quest(m1): evicted groups are a range miss, not a re-offer Re-offer stays limited to groups the cursor never sent; a relay refills groups it evicted through the subscribe-ranges miss fill. Co-Authored-By: Claude Opus 5.5 --- quest/m1/resolved-demand.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 6c931bac5e8..592a094fda0 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -52,7 +52,11 @@ with no wire change. [moq-dev/moq#5929](https://github.com/moq-dev/moq/pull/5929) adds for a widened range (a group outside the ranges is parked and re-offered when they widen); land after it and reuse that path rather than a second one. Only - groups this cursor never delivered come back. + groups this cursor never delivered come back, keeping #5929's rule that + nothing already served goes out twice; a group already delivered and since + evicted downstream is a cache miss, which the relay fills upstream by range + per [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) (a miss below + the cache becomes a range request), not a re-offer. - **Cost, accepted.** Groups below a newcomer's floor that the wider budget covers also come upstream, and the relay drops them locally. Bounded by the max age and the cache. @@ -79,7 +83,9 @@ floor below the copy's start and with none, gets the groups upstream still caches (fails on `main`); the same through a cold relay gives the same answer; a late group below the copy's start still reaches the no-floor reader; a reader joining with the same max age sends no SUBSCRIBE_UPDATE; and -a re-offered group is never delivered twice to one subscriber. Cover the +a re-offered group is never delivered twice to one subscriber; and a widen, +shrink, evict, widen sequence at the relay gets the evicted groups back +through the range miss fill, not a re-offer. Cover the warm-relay case in `js/net` too. Public API: none. Wire: no encoding change; a publisher now re-offers From d81db7567c49ef53e12f190325bb742fa5725078 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 13:46:02 -0700 Subject: [PATCH 06/24] quest(m1): re-offer follows the subscription's order Newest first when descending, lowest first only when ascending. Co-Authored-By: Claude Opus 5.5 --- quest/m1/resolved-demand.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 592a094fda0..e4269d51ce0 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -48,7 +48,9 @@ with no wire change. revisits them. Only an uncommitted conviction can come back. - **The fix.** When a subscriber's max age widens, re-offer the cached groups at or above its floor that its cursor gave up as stale and that the new - budget covers, lowest first. This extends the re-offer + budget covers, in the subscription's order: newest first when descending + (the default, so congestion sheds backlog rather than the live edge), and + lowest first only when ascending. This extends the re-offer [moq-dev/moq#5929](https://github.com/moq-dev/moq/pull/5929) adds for a widened range (a group outside the ranges is parked and re-offered when they widen); land after it and reuse that path rather than a second one. Only From 6a8476dd8f272af6d3c0965b8f89a6af9f550c33 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 13:55:20 -0700 Subject: [PATCH 07/24] quest(m1): re-offer when a stale verdict loses its edge Co-Authored-By: Claude Opus 5.5 --- quest/m1/resolved-demand.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index e4269d51ce0..36d7e783237 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -58,7 +58,10 @@ with no wire change. nothing already served goes out twice; a group already delivered and since evicted downstream is a cache miss, which the relay fills upstream by range per [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) (a miss below - the cache becomes a range request), not a re-offer. + the cache becomes a range request), not a re-offer. The same re-offer fires + at the same max age when a stale verdict loses its edge: the newer group + that made an older one stale is aborted or evicted, and `TrackState::is_stale` + revalidates the edge, so a cold cursor would serve the older group. - **Cost, accepted.** Groups below a newcomer's floor that the wider budget covers also come upstream, and the relay drops them locally. Bounded by the max age and the cache. @@ -87,11 +90,14 @@ answer; a late group below the copy's start still reaches the no-floor reader; a reader joining with the same max age sends no SUBSCRIBE_UPDATE; and a re-offered group is never delivered twice to one subscriber; and a widen, shrink, evict, widen sequence at the relay gets the evicted groups back -through the range miss fill, not a re-offer. Cover the +through the range miss fill, not a re-offer; and at the same max age, aborting +or evicting the group that made an older one stale re-offers the older one, +as a cold relay would serve it. Cover the warm-relay case in `js/net` too. Public API: none. Wire: no encoding change; a publisher now re-offers -cached groups when a SUBSCRIBE_UPDATE widens the max age. +cached groups when a SUBSCRIBE_UPDATE widens the max age or a stale verdict +loses its edge. ## Related From f25e30672bec518fea0050f589ff50a594ab16bb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 14:17:49 -0700 Subject: [PATCH 08/24] quest(m1): a widening outranks a reported drop Co-Authored-By: Claude Opus 5.5 --- quest/m1/resolved-demand.md | 42 +++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 36d7e783237..8cdb380219e 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -62,6 +62,40 @@ with no wire change. at the same max age when a stale verdict loses its edge: the newer group that made an older one stale is aborted or evicted, and `TrackState::is_stale` revalidates the edge, so a cold cursor would serve the older group. +- **A widening outranks a reported drop**, decided by the maintainer on + 2026-10-19. On lite-05 and lite-06 the publisher reports a stale skip with + SUBSCRIBE_DROP (lite-07 removed it), and today a reported sequence is + refused for good, so a re-offer alone would still lose the group. + - SUBSCRIBE_UPDATE stays incremental and never resets, since a reset races + in-flight, out-of-order data. A widening obliges the publisher to serve + the newly covered groups it still holds and never delivered on this + subscription, including ones it skipped as stale or reported in + SUBSCRIBE_DROP ("if we grow, we want that data"). Nothing delivered goes + out twice. + - A shrink stops serving what it removed without sending drops for it. + - Data wins over a drop. A drop rides the subscribe stream and groups their + own streams, so either can land first. A drop means "not coming under the + old terms": a group arriving after its drop is accepted, and a drop never + removes a group that already arrived. + - An evicted group comes back only through a separate past-range request + (subscribe ranges on lite-07, FETCH before it), never through the live + subscription, and arrivals are deduped by sequence. + + Code that changes: the Rust lite publisher's `TrackRun::served` + (`rs/moq-net/src/lite/publisher.rs`) holds served and dropped sequences in + one set and refuses any group in it, so split dropped from served and gate + only on served (`a_cut_dropped_then_recreated_is_not_served` flips); the + same for `#reported` in `js/net/src/track.ts`, whose `#tryRecvGroup` closes + any group already reported. On the model side, an arrival already clears + its drop (the insert path in `rs/moq-net/src/model/track.rs`, `#undrop` in + JS), but `TrackState::drop_groups` and JS `#drop` record the whole range, + so a drop landing after its group marks a cached group dropped and the + relay's cursor forwards it; both must skip sequences already cached. The + subscribers' SUBSCRIBE_DROP handling (`rs/moq-net/src/lite/subscriber.rs`, + `js/net/src/lite/subscriber.ts`) only accounts the tail, which must keep + accepting a later stream for an accounted sequence. No wire field and no + draft change: lite-07 has no SUBSCRIBE_DROP, and lite-05 and lite-06 are + published, with these rules affecting only behavior. - **Cost, accepted.** Groups below a newcomer's floor that the wider budget covers also come upstream, and the relay drops them locally. Bounded by the max age and the cache. @@ -92,12 +126,16 @@ a re-offered group is never delivered twice to one subscriber; and a widen, shrink, evict, widen sequence at the relay gets the evicted groups back through the range miss fill, not a re-offer; and at the same max age, aborting or evicting the group that made an older one stale re-offers the older one, -as a cold relay would serve it. Cover the +as a cold relay would serve it; on lite-05 and lite-06, a stale group whose +SUBSCRIBE_DROP is flushed before the widening SUBSCRIBE_UPDATE is still +served after it, and the same with the update first; a group arriving after +its drop is accepted, and a drop arriving after its group leaves it served; +a shrink sends no drops. Cover the warm-relay case in `js/net` too. Public API: none. Wire: no encoding change; a publisher now re-offers cached groups when a SUBSCRIBE_UPDATE widens the max age or a stale verdict -loses its edge. +loses its edge, including groups it reported in SUBSCRIBE_DROP. ## Related From 938767d6b59e7ed8aa2c3060565414aa1da0c1ee Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 14:24:09 -0700 Subject: [PATCH 09/24] quest(m1): scope drop re-offers to lite-07, relays re-judge stale drops Co-Authored-By: Claude Opus 5.5 --- quest/m1/resolved-demand.md | 98 +++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 8cdb380219e..33900656677 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -62,10 +62,15 @@ with no wire change. at the same max age when a stale verdict loses its edge: the newer group that made an older one stale is aborted or evicted, and `TrackState::is_stale` revalidates the edge, so a cold cursor would serve the older group. -- **A widening outranks a reported drop**, decided by the maintainer on - 2026-10-19. On lite-05 and lite-06 the publisher reports a stale skip with - SUBSCRIBE_DROP (lite-07 removed it), and today a reported sequence is - refused for good, so a re-offer alone would still lose the group. +- **On lite-07, a widening outranks a reported drop**, decided by the + maintainer on 2026-10-19. lite-07 brings SUBSCRIBE_DROP back + ([SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md)), + and a publisher reports a stale skip with it, so a re-offer alone would + still lose a group already reported. These rules hold only on a lite-07 + hop, which is unpublished and free to change. Released lite-05 and lite-06 + peers treat a flushed drop as final, so on those versions a group already + reported dropped stays dropped, as today, and the re-offer covers only stale + skips not yet reported. - SUBSCRIBE_UPDATE stays incremental and never resets, since a reset races in-flight, out-of-order data. A widening obliges the publisher to serve the newly covered groups it still holds and never delivered on this @@ -77,25 +82,54 @@ with no wire change. own streams, so either can land first. A drop means "not coming under the old terms": a group arriving after its drop is accepted, and a drop never removes a group that already arrived. + - **A relay never forwards a stale verdict; each reader judges its own.** A + warm relay that forwards a stored stale drop would fail a newly widened + reader before the upstream re-offer arrives. Scoping the drop by the + budget it was judged under does not hold without a new field: the + widening SUBSCRIBE_UPDATE and the drop cross on one bidi stream with no + acknowledgement, so the relay cannot tell which budget a drop arriving + after its update was judged under. Instead the drop says it is a + staleness verdict through its existing Error Code (OLD). The relay + accounts it in its upstream tail but does not record it as a track drop, + and each downstream cursor judges the missing group against its own + budget, which `TrackState::drifted` already does for a group the track + never received. A reader no wider than the upstream budget sees the + group go stale under its own budget and gets its own drop, as from a + cold relay. A wider reader waits for the re-offer, or for a drop the + upstream sends under the new terms for a group it no longer holds. + Drops for any other reason (a cut, a group never produced) do not depend + on the budget and are forwarded as today. No new wire field. - An evicted group comes back only through a separate past-range request (subscribe ranges on lite-07, FETCH before it), never through the live subscription, and arrivals are deduped by sequence. - Code that changes: the Rust lite publisher's `TrackRun::served` - (`rs/moq-net/src/lite/publisher.rs`) holds served and dropped sequences in - one set and refuses any group in it, so split dropped from served and gate - only on served (`a_cut_dropped_then_recreated_is_not_served` flips); the - same for `#reported` in `js/net/src/track.ts`, whose `#tryRecvGroup` closes - any group already reported. On the model side, an arrival already clears - its drop (the insert path in `rs/moq-net/src/model/track.rs`, `#undrop` in - JS), but `TrackState::drop_groups` and JS `#drop` record the whole range, - so a drop landing after its group marks a cached group dropped and the - relay's cursor forwards it; both must skip sequences already cached. The - subscribers' SUBSCRIBE_DROP handling (`rs/moq-net/src/lite/subscriber.rs`, - `js/net/src/lite/subscriber.ts`) only accounts the tail, which must keep - accepting a later stream for an accounted sequence. No wire field and no - draft change: lite-07 has no SUBSCRIBE_DROP, and lite-05 and lite-06 are - published, with these rules affecting only behavior. + Code that changes, gated to lite-07 hops: + - The Rust lite publisher's `TrackRun::served` + (`rs/moq-net/src/lite/publisher.rs`) holds served and dropped sequences + in one set and refuses any group in it. Split dropped from served and + gate only on served; `a_cut_dropped_then_recreated_is_not_served` keeps + its rule on lite-05 and lite-06. Do the same for `#reported` in + `js/net/src/track.ts`, whose `#tryRecvGroup` closes any group already + reported. + - A widening re-reports, under the new terms, the newly covered sequences + the publisher cannot serve. + - On the model side, an arrival already clears its drop (the insert path in + `rs/moq-net/src/model/track.rs`, `#undrop` in JS). `TrackState::drop_groups` + and JS `#drop` record the whole range, though, so a drop landing after its + group marks a cached group dropped and the relay's cursor forwards it. + Both must skip sequences already cached and keep OLD drops out of the + track's drops. + - The subscribers' SUBSCRIBE_DROP handling (`rs/moq-net/src/lite/subscriber.rs`, + `js/net/src/lite/subscriber.ts`) only accounts the tail, which must keep + accepting a later stream for an accounted sequence. + + Draft wording is in scope: `drafts/draft-lcurley-moq-lite.md` states, for + lite-07's SUBSCRIBE_DROP, that a drop holds only under the terms in effect + when sent, that a widening re-offers held groups and re-drops the rest, + that a later group wins over its drop, and that a relay does not forward an + OLD drop. Whichever of this and + [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) + lands second writes these rules into the restored message. - **Cost, accepted.** Groups below a newcomer's floor that the wider budget covers also come upstream, and the relay drops them locally. Bounded by the max age and the cache. @@ -126,18 +160,30 @@ a re-offered group is never delivered twice to one subscriber; and a widen, shrink, evict, widen sequence at the relay gets the evicted groups back through the range miss fill, not a re-offer; and at the same max age, aborting or evicting the group that made an older one stale re-offers the older one, -as a cold relay would serve it; on lite-05 and lite-06, a stale group whose -SUBSCRIBE_DROP is flushed before the widening SUBSCRIBE_UPDATE is still -served after it, and the same with the update first; a group arriving after -its drop is accepted, and a drop arriving after its group leaves it served; -a shrink sends no drops. Cover the +as a cold relay would serve it. On lite-07: +- A widened reader receives a stale group whose SUBSCRIBE_DROP was flushed + before the widening SUBSCRIBE_UPDATE, and the same with the update first. +- A group arriving after its drop is accepted, and a drop arriving after its + group leaves it served. +- At a warm relay holding an OLD drop, a newly widened reader gets the + re-offered group and never the drop, while a reader no wider than the + upstream budget still gets its own drop. Check what the reader receives, + not only the tail accounting. +- A shrink sends no drops. + +Mixed versions: through a lite-06 hop, a group already reported dropped stays +dropped for the reader, with no session error, and a lite-07 hop never sends +a re-offered group onto a lite-06 hop after reporting it. Cover the warm-relay case in `js/net` too. -Public API: none. Wire: no encoding change; a publisher now re-offers +Public API: none. Wire: no encoding change. A publisher now re-offers cached groups when a SUBSCRIBE_UPDATE widens the max age or a stale verdict -loses its edge, including groups it reported in SUBSCRIBE_DROP. +loses its edge. On lite-07 that includes groups it reported in +SUBSCRIBE_DROP, with the draft wording above; lite-05 and lite-06 keep a +reported drop final. ## Related - [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - #5929 lands its serving half, including the re-offer this extends - [Archive producer](/quest/m1/archive/producer.md) - its resume floor crosses relays that viewers may have warmed +- [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - restores the lite-07 drop whose widening and relay rules this sets From ccf7ab0e452fc63841434a202023d19df0a29bc9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 14:56:24 -0700 Subject: [PATCH 10/24] quest(m1): drops are final for now, plan the drop set Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/drop-set.md | 107 ++++++++++++++++++++++++++++++++++++ quest/m1/resolved-demand.md | 103 ++++++---------------------------- 3 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 quest/m1/drop-set.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 3833100ba8f..c495804d743 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -109,6 +109,7 @@ lands before the cut. - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide - [Resume reorder](/quest/m1/resume-reorder.md) - a group whose stream header arrives after its successor's still resumes on failover; only a dropped or long-gone group fails - [A widened max age re-offers cached groups](/quest/m1/resolved-demand.md) - a warm relay serves a reader with a wider max age the cached groups a cold relay would, with no wire change +- [Drop set](/quest/m1/drop-set.md) - each cursor judges missing groups against its own budget, so a narrow reader drops promptly and a widening re-offers groups already reported dropped on lite-07 - [Received timestamps are media time](/quest/m1/lite-time-provenance.md) - a received timed track's timestamps are media time, so the rewind check can run across hops - [WebGPU on Safari](/quest/m1/webgpu-safari.md) - the WebGPU renderer is verified on Safari 26 for macOS and iOS - [HLS export drains after End](/quest/m1/hls-export-end-drain.md) - a cleanly ended broadcast still exports its retained segments diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md new file mode 100644 index 00000000000..1b8821826ff --- /dev/null +++ b/quest/m1/drop-set.md @@ -0,0 +1,107 @@ +# [L] Drop set + +## Goal + +Every cursor judges a missing group against its own budget. Today a dropped +group is final for every reader of a track: a narrow reader can wait +indefinitely for an upstream OLD naming a group the relay never cached, +and a wider reader never gets a group an upstream already reported stale. +The track state instead keeps the dropped and expired runs with their +reason and budget, so a narrow reader gets a prompt drop, a wide reader +waits, and a widening re-offers what its new budget covers. On lite-07 this +restores "growing means I want it": a group reported dropped under a +narrower budget is served after a widening, as a cold relay would. + +## Plan + +Decided by the maintainer on 2026-10-19, splitting this from +[A widened max age re-offers cached groups](/quest/m1/resolved-demand.md), +which keeps drops final for now. + +- **One run-length set per track.** The track state keeps the dropped and + expired group runs within the subscription range. Each run carries its + reason (stale, cut, never produced, evicted) and, for a stale run, the + budget it was judged under. A group that arrives removes its sequence; a + drop never removes a group that already arrived. +- **Each cursor judges for itself.** A cursor enumerates the missing + sequences in its range from the set: + - A run whose reason does not depend on the budget (a cut, a group never + produced) is dropped for every reader, as today. + - A stale run is dropped promptly for a reader whose budget is no wider + than the one it was judged under, and for any reader under whose own + budget `TrackState::drifted` convicts it. + - A wider reader waits for the group, a re-offer, or its own verdict. + + This fixes the gap Codex found on + [moq-dev/moq#5986](https://github.com/moq-dev/moq/pull/5986): today + `TrackRun::poll_forward_drop` emits only recorded drops, cuts, or stale + cached groups. A sequence the relay never cached is not judged until the + track finishes. +- **A widening re-offers the dropped runs now inside its budget,** through + the re-offer path the resolved-demand quest adds, in the subscription's + order. Upstream, the relay's own widening asks for them again. +- **A received drop's budget.** A SUBSCRIBE_UPDATE has no acknowledgement, + so a drop that arrives after a widening may have been judged under either + budget. Record the narrowest budget sent upstream since the last widening. + A guess that is too narrow only makes a wide reader wait for its own + verdict, never drop early. +- **lite-07 only for re-offering reported groups.** Released lite-05 and + lite-06 peers treat a flushed SUBSCRIBE_DROP as final, so a drop received + over those versions is final for every reader, and a lite-07 hop never + sends a group onto a lite-05 or lite-06 hop after reporting it. On + lite-07, a widening obliges the publisher to serve the newly covered groups + it holds and never delivered on this subscription, including ones it + reported dropped, and to drop again, under the new terms, the ones it + cannot serve. SUBSCRIBE_UPDATE stays incremental and never resets, since a + reset races in-flight, out-of-order data. A shrink sends no drops. +- **Draft wording.** `drafts/draft-lcurley-moq-lite.md` states, for lite-07's + restored SUBSCRIBE_DROP, that a drop holds only under the terms in effect + when it was sent, that a widening re-offers held groups and re-drops the + rest, and that a later group wins over its drop. Whichever of this and + [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) + lands second writes it. No new wire field. + +Code it replaces, in Rust and `js/net` alike: + +- `TrackState::drop_groups`, the `drops` range set and `DropCursor` in + `rs/moq-net/src/model/track.rs`, which record a whole range without its + reason or budget, even over groups already cached. +- `TrackRun::served` in `rs/moq-net/src/lite/publisher.rs`, which holds + served and dropped sequences in one set and refuses any group in it. +- In `js/net/src/track.ts`: `#drop`, `#undrop`, the `drops` signal, and the + wire cursor's `#pending`, `#dropsSeen`, and `#reported`, whose + `#tryRecvGroup` closes any group already reported. + +Untrusted input: a hostile peer can fragment the set by alternating drops +and arrivals. The set never holds more runs than the groups in the +subscription range, the same order as the cache index, so it needs no limit +constant. Extend `track_subscriber_churn` in `rs/moq-net/benches/track.rs` +with a fragmented set, swept over subscriber count and runs, so enumeration +cost tracks the touched runs, not the set. + +Test on mock time through a relay: +- On a live track, a reader no wider than the upstream budget gets a prompt + drop for an upstream OLD naming a group the relay never cached. +- On lite-07, a widened reader gets that group re-offered and never the + drop, whether the drop was flushed before or after the widening + SUBSCRIBE_UPDATE. Check what each reader receives, not only tail + accounting. +- A group arriving after its drop is served, and a drop arriving after its + group leaves it served. +- Through a lite-06 hop, a reported group stays dropped for every reader, + with no session error. +- A shrink sends no drops. +- Alternating drops and arrivals never grow the set past the range. + +Mirror each case in `js/net`. + +Public API: none. Wire: no encoding change; lite-07 SUBSCRIBE_DROP gains the +widening and ordering rules above. + +## Required + +- [A widened max age re-offers cached groups](/quest/m1/resolved-demand.md) - the re-offer path this extends to reported groups + +## Related + +- [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - restores the lite-07 drop these rules apply to diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 33900656677..d298d521353 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -10,6 +10,8 @@ with a floor at 5 or with no floor at all, gets nothing below 10. The relay does send the wider max age upstream in SUBSCRIBE_UPDATE, but the upstream cursor never re-offers the cached groups it already skipped as stale. The reader silently starts later than it asked, and only through a warm relay. +Groups the upstream already reported dropped stay out of scope here (see +[Drop set](/quest/m1/drop-set.md)). An archive resuming at its last stored frame through a relay that a viewer already warmed hits this, as does any reader with a wider max age than the @@ -62,74 +64,14 @@ with no wire change. at the same max age when a stale verdict loses its edge: the newer group that made an older one stale is aborted or evicted, and `TrackState::is_stale` revalidates the edge, so a cold cursor would serve the older group. -- **On lite-07, a widening outranks a reported drop**, decided by the - maintainer on 2026-10-19. lite-07 brings SUBSCRIBE_DROP back - ([SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md)), - and a publisher reports a stale skip with it, so a re-offer alone would - still lose a group already reported. These rules hold only on a lite-07 - hop, which is unpublished and free to change. Released lite-05 and lite-06 - peers treat a flushed drop as final, so on those versions a group already - reported dropped stays dropped, as today, and the re-offer covers only stale - skips not yet reported. - - SUBSCRIBE_UPDATE stays incremental and never resets, since a reset races - in-flight, out-of-order data. A widening obliges the publisher to serve - the newly covered groups it still holds and never delivered on this - subscription, including ones it skipped as stale or reported in - SUBSCRIBE_DROP ("if we grow, we want that data"). Nothing delivered goes - out twice. - - A shrink stops serving what it removed without sending drops for it. - - Data wins over a drop. A drop rides the subscribe stream and groups their - own streams, so either can land first. A drop means "not coming under the - old terms": a group arriving after its drop is accepted, and a drop never - removes a group that already arrived. - - **A relay never forwards a stale verdict; each reader judges its own.** A - warm relay that forwards a stored stale drop would fail a newly widened - reader before the upstream re-offer arrives. Scoping the drop by the - budget it was judged under does not hold without a new field: the - widening SUBSCRIBE_UPDATE and the drop cross on one bidi stream with no - acknowledgement, so the relay cannot tell which budget a drop arriving - after its update was judged under. Instead the drop says it is a - staleness verdict through its existing Error Code (OLD). The relay - accounts it in its upstream tail but does not record it as a track drop, - and each downstream cursor judges the missing group against its own - budget, which `TrackState::drifted` already does for a group the track - never received. A reader no wider than the upstream budget sees the - group go stale under its own budget and gets its own drop, as from a - cold relay. A wider reader waits for the re-offer, or for a drop the - upstream sends under the new terms for a group it no longer holds. - Drops for any other reason (a cut, a group never produced) do not depend - on the budget and are forwarded as today. No new wire field. - - An evicted group comes back only through a separate past-range request - (subscribe ranges on lite-07, FETCH before it), never through the live - subscription, and arrivals are deduped by sequence. - - Code that changes, gated to lite-07 hops: - - The Rust lite publisher's `TrackRun::served` - (`rs/moq-net/src/lite/publisher.rs`) holds served and dropped sequences - in one set and refuses any group in it. Split dropped from served and - gate only on served; `a_cut_dropped_then_recreated_is_not_served` keeps - its rule on lite-05 and lite-06. Do the same for `#reported` in - `js/net/src/track.ts`, whose `#tryRecvGroup` closes any group already - reported. - - A widening re-reports, under the new terms, the newly covered sequences - the publisher cannot serve. - - On the model side, an arrival already clears its drop (the insert path in - `rs/moq-net/src/model/track.rs`, `#undrop` in JS). `TrackState::drop_groups` - and JS `#drop` record the whole range, though, so a drop landing after its - group marks a cached group dropped and the relay's cursor forwards it. - Both must skip sequences already cached and keep OLD drops out of the - track's drops. - - The subscribers' SUBSCRIBE_DROP handling (`rs/moq-net/src/lite/subscriber.rs`, - `js/net/src/lite/subscriber.ts`) only accounts the tail, which must keep - accepting a later stream for an accounted sequence. - - Draft wording is in scope: `drafts/draft-lcurley-moq-lite.md` states, for - lite-07's SUBSCRIBE_DROP, that a drop holds only under the terms in effect - when sent, that a widening re-offers held groups and re-drops the rest, - that a later group wins over its drop, and that a relay does not forward an - OLD drop. Whichever of this and - [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - lands second writes these rules into the restored message. +- **Drops are final for now**, decided by the maintainer on 2026-10-19. A + widening re-offers only groups this cursor never reported in + SUBSCRIBE_DROP; a reported group stays dropped, as today. That leaves a + known warm/cold gap: on lite-05 and lite-06 the publisher reports a stale + skip as soon as it gives it up, so a group already reported is not + re-offered, while a cold relay would serve it. Closing that gap needs + per-cursor judgement of missing sequences, planned in + [Drop set](/quest/m1/drop-set.md). - **Cost, accepted.** Groups below a newcomer's floor that the wider budget covers also come upstream, and the relay drops them locally. Bounded by the max age and the cache. @@ -160,30 +102,17 @@ a re-offered group is never delivered twice to one subscriber; and a widen, shrink, evict, widen sequence at the relay gets the evicted groups back through the range miss fill, not a re-offer; and at the same max age, aborting or evicting the group that made an older one stale re-offers the older one, -as a cold relay would serve it. On lite-07: -- A widened reader receives a stale group whose SUBSCRIBE_DROP was flushed - before the widening SUBSCRIBE_UPDATE, and the same with the update first. -- A group arriving after its drop is accepted, and a drop arriving after its - group leaves it served. -- At a warm relay holding an OLD drop, a newly widened reader gets the - re-offered group and never the drop, while a reader no wider than the - upstream budget still gets its own drop. Check what the reader receives, - not only the tail accounting. -- A shrink sends no drops. - -Mixed versions: through a lite-06 hop, a group already reported dropped stays -dropped for the reader, with no session error, and a lite-07 hop never sends -a re-offered group onto a lite-06 hop after reporting it. Cover the +as a cold relay would serve it; and a stale group already reported in +SUBSCRIBE_DROP stays dropped after a widening, never served after its drop. +Cover the warm-relay case in `js/net` too. Public API: none. Wire: no encoding change. A publisher now re-offers -cached groups when a SUBSCRIBE_UPDATE widens the max age or a stale verdict -loses its edge. On lite-07 that includes groups it reported in -SUBSCRIBE_DROP, with the draft wording above; lite-05 and lite-06 keep a -reported drop final. +cached groups it never reported dropped when a SUBSCRIBE_UPDATE widens the +max age or a stale verdict loses its edge. ## Related - [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - #5929 lands its serving half, including the re-offer this extends - [Archive producer](/quest/m1/archive/producer.md) - its resume floor crosses relays that viewers may have warmed -- [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - restores the lite-07 drop whose widening and relay rules this sets +- [Drop set](/quest/m1/drop-set.md) - closes the gap this leaves for groups already reported dropped From 9e2557bd4f28357b2c9fecf04f78df993d34386a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 15:07:42 -0700 Subject: [PATCH 11/24] quest(m1): stale drops name their budget, drop runs bill the pool Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 58 ++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 1b8821826ff..16252aae851 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -40,11 +40,21 @@ which keeps drops final for now. - **A widening re-offers the dropped runs now inside its budget,** through the re-offer path the resolved-demand quest adds, in the subscription's order. Upstream, the relay's own widening asks for them again. -- **A received drop's budget.** A SUBSCRIBE_UPDATE has no acknowledgement, - so a drop that arrives after a widening may have been judged under either - budget. Record the narrowest budget sent upstream since the last widening. - A guess that is too narrow only makes a wide reader wait for its own - verdict, never drop early. +- **A stale drop names its budget.** A SUBSCRIBE_UPDATE has no + acknowledgement, so after a widening from B1 to B2, an in-flight B1 drop + and the B2 re-drop look identical, and no guess settles a B2 reader. On + lite-07 SUBSCRIBE_DROP carries `Max Age`: the budget a staleness verdict + was judged under, encoded like `Publisher Max Age` (milliseconds plus one), + with `0` for a drop that does not depend on the budget (a cut, a group + never produced). The relay records each run with that exact budget. A B1 + drop then settles only readers no wider than B1, and the B2 re-drop + settles a B2 reader. The cost is one varint per drop, a few bytes on an + event that is already per group, which is worth an unambiguous term. + Rejected: an implicit generation counted by both sides per + SUBSCRIBE_UPDATE. It needs a generation-to-budget history at the relay + and agreement on every update counted, while the budget itself is + self-describing, order-free, and unaffected by updates that change + nothing. - **lite-07 only for re-offering reported groups.** Released lite-05 and lite-06 peers treat a flushed SUBSCRIBE_DROP as final, so a drop received over those versions is final for every reader, and a lite-07 hop never @@ -59,7 +69,8 @@ which keeps drops final for now. when it was sent, that a widening re-offers held groups and re-drops the rest, and that a later group wins over its drop. Whichever of this and [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - lands second writes it. No new wire field. + lands second writes it, including the `Max Age` field above, which is + free to add because lite-07 is unpublished. Code it replaces, in Rust and `js/net` alike: @@ -72,12 +83,22 @@ Code it replaces, in Rust and `js/net` alike: wire cursor's `#pending`, `#dropsSeen`, and `#reported`, whose `#tryRecvGroup` closes any group already reported. -Untrusted input: a hostile peer can fragment the set by alternating drops -and arrivals. The set never holds more runs than the groups in the -subscription range, the same order as the cache index, so it needs no limit -constant. Extend `track_subscriber_churn` in `rs/moq-net/benches/track.rs` -with a fragmented set, swept over subscriber count and runs, so enumeration -cost tracks the touched runs, not the set. +Untrusted input: a hostile upstream can fragment the set by alternating +drops and arrivals, and an open live range is no bound, since its groups are +evicted while their runs would stay. Bill each run to the cache's existing +byte accounting like a group: the Rust `Pool` through a `Charge` at a fixed +per-run cost, and the same eviction bound in `js/net`. Under pressure the +pool evicts the oldest runs as it evicts groups, and a run older than the +cache's retention window is forgotten, so the set never outgrows what the +pool would hold, with no new constant. An evicted run's sequences become +unknown again. A cursor judges an unknown missing sequence as it would a +group never received: locally against its own budget (`TrackState::drifted`), +and otherwise as a cache miss, which the relay fills upstream by range +([Subscribe ranges](/quest/m1/subscribe-ranges/README.md)), so no reader +waits indefinitely on forgotten state. Extend `track_subscriber_churn` in +`rs/moq-net/benches/track.rs` with a fragmented set up to pool capacity, +swept over subscriber count and runs, so enumeration cost tracks the touched +runs, not the set. Test on mock time through a relay: - On a live track, a reader no wider than the upstream budget gets a prompt @@ -86,17 +107,23 @@ Test on mock time through a relay: drop, whether the drop was flushed before or after the widening SUBSCRIBE_UPDATE. Check what each reader receives, not only tail accounting. +- On lite-07, after a widening from B1 to B2 for a group the upstream no + longer holds, an in-flight drop tagged B1 settles only the B1 reader, and + the re-drop tagged B2 settles the B2 reader. - A group arriving after its drop is served, and a drop arriving after its group leaves it served. - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. -- Alternating drops and arrivals never grow the set past the range. +- A flood of fragmented runs stays within the pool, evicting the oldest + runs first, and a sequence whose run was evicted is judged locally or + filled by range, never waited on indefinitely. Mirror each case in `js/net`. -Public API: none. Wire: no encoding change; lite-07 SUBSCRIBE_DROP gains the -widening and ordering rules above. +Public API: none. Wire: lite-07 SUBSCRIBE_DROP (unpublished) gains `Max Age` +and the widening and ordering rules above; published versions are +unchanged. ## Required @@ -105,3 +132,4 @@ widening and ordering rules above. ## Related - [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - restores the lite-07 drop these rules apply to +- [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - fills the cache miss an evicted run leaves From e87ed609f4e938df0a2a62ab21a8daa4f6747fe8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 15:42:17 -0700 Subject: [PATCH 12/24] quest(m1): a stale drop that loses its edge is invalidated Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 16252aae851..8fc4997bde5 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -40,6 +40,17 @@ which keeps drops final for now. - **A widening re-offers the dropped runs now inside its budget,** through the re-offer path the resolved-demand quest adds, in the subscription's order. Upstream, the relay's own widening asks for them again. +- **A stale run that loses its edge is invalidated,** the same as a widening + invalidates it, matching resolved-demand's stale-edge trigger. Each stale + run records the edge it was judged against: for a local verdict, the + newer group that convicted it; for a received drop, this hop's newest group + when the drop arrived. When that group is aborted or evicted here, so the + edge moves back past the record, the run's sequences become unknown: a + cached group is re-offered, and a missing one is judged again against the + current edge (`TrackState::drifted`) or waited for. Across hops, the + upstream applies the same rule on lite-07 and re-offers the group, which + wins over the drop downstream too. Cuts and never-produced runs carry no + edge and are unaffected. No wire field: the record is local. - **A stale drop names its budget.** A SUBSCRIBE_UPDATE has no acknowledgement, so after a widening from B1 to B2, an in-flight B1 drop and the B2 re-drop look identical, and no guess settles a B2 reader. On @@ -112,6 +123,11 @@ Test on mock time through a relay: the re-drop tagged B2 settles the B2 reader. - A group arriving after its drop is served, and a drop arriving after its group leaves it served. +- Cross-hop, at the same budget B: a group G reported stale because of a + newer edge E is served to a B reader once E is aborted or evicted, both + when E is lost at the relay (the run is invalidated and G re-offered from + the cache) and when E is lost upstream (the upstream re-offers G, which + wins over the stored drop), as a cold relay would serve it. - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. From 468ec989efb16422b0c58a3a3a4dfcdd634a0e1a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 15:47:13 -0700 Subject: [PATCH 13/24] quest(m1): drop-set cursors settle whole runs, resolved-demand requires dynamic ranges Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 36 +++++++++++++++++++++++++----------- quest/m1/resolved-demand.md | 5 ++++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 8fc4997bde5..cff408b2bd6 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -23,14 +23,25 @@ which keeps drops final for now. reason (stale, cut, never produced, evicted) and, for a stale run, the budget it was judged under. A group that arrives removes its sequence; a drop never removes a group that already arrived. -- **Each cursor judges for itself.** A cursor enumerates the missing - sequences in its range from the set: +- **Each cursor judges for itself, a run at a time.** A cursor walks the + set's runs in its range, never their sequences, and settles each whole run + in O(1): - A run whose reason does not depend on the budget (a cut, a group never - produced) is dropped for every reader, as today. - - A stale run is dropped promptly for a reader whose budget is no wider - than the one it was judged under, and for any reader under whose own - budget `TrackState::drifted` convicts it. - - A wider reader waits for the group, a re-offer, or its own verdict. + produced) is dropped for every reader as one range, as today. + - A stale run is dropped promptly as one range for a reader whose budget + is no wider than the one it was judged under, and for any reader under + whose own budget `TrackState::drifted` convicts it. `drifted` judges a + missing sequence by its successor here, and every sequence in a missing + run shares one successor, so one call settles the run. + - A wider reader skips the run and waits for a group, a re-offer, or its + own verdict, each of which splits the run at most once. + - An unknown span (a gap between runs, or an evicted run) is judged the + same way, as one unit bounded by its neighbours: by one `drifted` call + against its shared successor, or by one range miss fill upstream. + + A hostile upstream's huge run costs one Charge and O(1) per cursor, and + splitting it costs per-run Charges the pool bounds, so CPU tracks pool + bytes, never span. This fixes the gap Codex found on [moq-dev/moq#5986](https://github.com/moq-dev/moq/pull/5986): today @@ -40,8 +51,8 @@ which keeps drops final for now. - **A widening re-offers the dropped runs now inside its budget,** through the re-offer path the resolved-demand quest adds, in the subscription's order. Upstream, the relay's own widening asks for them again. -- **A stale run that loses its edge is invalidated,** the same as a widening - invalidates it, matching resolved-demand's stale-edge trigger. Each stale +- **A stale run that loses its edge is invalidated** as one span, the same + as a widening invalidates it, matching resolved-demand's stale-edge trigger. Each stale run records the edge it was judged against: for a local verdict, the newer group that convicted it; for a received drop, this hop's newest group when the drop arrived. When that group is aborted or evicted here, so the @@ -108,8 +119,8 @@ and otherwise as a cache miss, which the relay fills upstream by range ([Subscribe ranges](/quest/m1/subscribe-ranges/README.md)), so no reader waits indefinitely on forgotten state. Extend `track_subscriber_churn` in `rs/moq-net/benches/track.rs` with a fragmented set up to pool capacity, -swept over subscriber count and runs, so enumeration cost tracks the touched -runs, not the set. +swept over subscriber count, run count, and run span, so enumeration cost +tracks the touched runs, never the set or a run's span. Test on mock time through a relay: - On a live track, a reader no wider than the upstream budget gets a prompt @@ -134,6 +145,9 @@ Test on mock time through a relay: - A flood of fragmented runs stays within the pool, evicting the oldest runs first, and a sequence whose run was evicted is judged locally or filled by range, never waited on indefinitely. +- A single run spanning billions of sequences is settled by each cursor in + one step, whether dropped, skipped by a wider reader, or evicted and + judged again. Mirror each case in `js/net`. diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index d298d521353..790a464f445 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -111,8 +111,11 @@ Public API: none. Wire: no encoding change. A publisher now re-offers cached groups it never reported dropped when a SUBSCRIBE_UPDATE widens the max age or a stale verdict loses its edge. +## Required + +- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - #5929 lands the re-offer and serving path and the range miss fill this reuses + ## Related -- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - #5929 lands its serving half, including the re-offer this extends - [Archive producer](/quest/m1/archive/producer.md) - its resume floor crosses relays that viewers may have warmed - [Drop set](/quest/m1/drop-set.md) - closes the gap this leaves for groups already reported dropped From 5e3ed357f0712dafac51586bc0433a10b3ff33e5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 15:52:06 -0700 Subject: [PATCH 14/24] quest(m1): received stale drops record no edge, drop-set requires subscribe-drop Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index cff408b2bd6..8c853b3e035 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -51,17 +51,20 @@ which keeps drops final for now. - **A widening re-offers the dropped runs now inside its budget,** through the re-offer path the resolved-demand quest adds, in the subscription's order. Upstream, the relay's own widening asks for them again. -- **A stale run that loses its edge is invalidated** as one span, the same - as a widening invalidates it, matching resolved-demand's stale-edge trigger. Each stale - run records the edge it was judged against: for a local verdict, the - newer group that convicted it; for a received drop, this hop's newest group - when the drop arrived. When that group is aborted or evicted here, so the - edge moves back past the record, the run's sequences become unknown: a - cached group is re-offered, and a missing one is judged again against the - current edge (`TrackState::drifted`) or waited for. Across hops, the - upstream applies the same rule on lite-07 and re-offers the group, which - wins over the drop downstream too. Cuts and never-produced runs carry no - edge and are unaffected. No wire field: the record is local. +- **A local stale run that loses its edge is invalidated** as one span, the + same as a widening invalidates it, matching resolved-demand's stale-edge + trigger. A run this hop judged stale records the newer group here that + convicted it. When that group is aborted or evicted, the run's sequences + become unknown: a cached group is re-offered, and a missing one is judged + again against the current edge (`TrackState::drifted`) or waited for. +- **A received stale run records no edge.** Its groups are not held here, + so a cold cursor at this relay would fill them from upstream by range, and + the upstream, still holding its edge, would drop them again. A local abort + or eviction therefore never makes a received verdict obsolete; only the + upstream changing its mind does, and it says so: on lite-07 it re-offers + the group when its own edge is lost, and a widening re-asks. A received + run is invalidated only by an upstream re-offer or a widening. Cuts and + never-produced runs carry no edge and are unaffected. No wire field. - **A stale drop names its budget.** A SUBSCRIBE_UPDATE has no acknowledgement, so after a widening from B1 to B2, an in-flight B1 drop and the B2 re-drop look identical, and no guess settles a B2 reader. On @@ -134,11 +137,12 @@ Test on mock time through a relay: the re-drop tagged B2 settles the B2 reader. - A group arriving after its drop is served, and a drop arriving after its group leaves it served. -- Cross-hop, at the same budget B: a group G reported stale because of a - newer edge E is served to a B reader once E is aborted or evicted, both - when E is lost at the relay (the run is invalidated and G re-offered from - the cache) and when E is lost upstream (the upstream re-offers G, which - wins over the stored drop), as a cold relay would serve it. +- At the same budget B, for a group G judged stale because of a newer + edge E: when the relay judged G itself and E is aborted or evicted there, + a B reader gets G re-offered from the cache. When G was reported stale by + the upstream and E is lost upstream, the upstream re-offers G, which wins + over the stored drop. When E is evicted only at the relay, the received + drop stands, as a cold relay filling G from upstream would also be told. - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. @@ -158,8 +162,8 @@ unchanged. ## Required - [A widened max age re-offers cached groups](/quest/m1/resolved-demand.md) - the re-offer path this extends to reported groups +- [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - restores the lite-07 drop whose wire and behavior this extends ## Related -- [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - restores the lite-07 drop these rules apply to - [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - fills the cache miss an evicted run leaves From 09a86427381ea8e1900846ff8827e38a5a426d58 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 16:24:01 -0700 Subject: [PATCH 15/24] quest(m1): each cursor keeps a disposition record next to the drop set Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 67 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 8c853b3e035..23843373d72 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -48,13 +48,31 @@ which keeps drops final for now. `TrackRun::poll_forward_drop` emits only recorded drops, cuts, or stale cached groups. A sequence the relay never cached is not judged until the track finishes. +- **Each cursor keeps its own disposition record,** next to the per-track + set, as the maintainer first framed it ("keep what was dropped within each + cursor and within the track state"). The shared set cannot hold it: a + group that arrives leaves the shared set, so it cannot remember that this + cursor reported cached G stale, and after a widening or edge loss the + cursor could not tell G (re-offer it) from a group it delivered (never + resend it). The record is one run-length map per cursor, each run marked + delivered or reported, a reported run carrying its reason, the budget it + was judged under, and for a local stale verdict the convicting edge. It + replaces today's `Delivered` (`rs/moq-net/src/model/delivered.rs`), whose + floor (`forget_below`) folds stale gaps in with delivered groups, and + absorbs `TrackRun::served` and JS `#reported`. Delivery must be recorded, + not inferred from the cursor position: an arrival-order or descending + cursor delivers out of sequence order, and #5929 re-offers below the + position, so only the delivered runs say what never goes out twice. The + record only answers "re-offer or not"; the serving and re-offer path stays + #5929's. - **A widening re-offers the dropped runs now inside its budget,** through the re-offer path the resolved-demand quest adds, in the subscription's order. Upstream, the relay's own widening asks for them again. - **A local stale run that loses its edge is invalidated** as one span, the same as a widening invalidates it, matching resolved-demand's stale-edge - trigger. A run this hop judged stale records the newer group here that - convicted it. When that group is aborted or evicted, the run's sequences + trigger. A run this cursor judged stale records, in its disposition + record, the newer group here that convicted it. When that group is + aborted or evicted, the run's sequences become unknown: a cached group is re-offered, and a missing one is judged again against the current edge (`TrackState::drifted`) or waited for. - **A received stale run records no edge.** Its groups are not held here, @@ -102,11 +120,14 @@ Code it replaces, in Rust and `js/net` alike: - `TrackState::drop_groups`, the `drops` range set and `DropCursor` in `rs/moq-net/src/model/track.rs`, which record a whole range without its reason or budget, even over groups already cached. -- `TrackRun::served` in `rs/moq-net/src/lite/publisher.rs`, which holds - served and dropped sequences in one set and refuses any group in it. -- In `js/net/src/track.ts`: `#drop`, `#undrop`, the `drops` signal, and the - wire cursor's `#pending`, `#dropsSeen`, and `#reported`, whose - `#tryRecvGroup` closes any group already reported. +- `Delivered` in `rs/moq-net/src/model/delivered.rs` and + `TrackRun::served` in `rs/moq-net/src/lite/publisher.rs`, which hold + served and dropped sequences in one set and refuse any group in it. Both + become the per-cursor disposition record, which tells them apart. +- In `js/net/src/track.ts`: `#drop`, `#undrop`, and the `drops` signal go; + the wire cursor's `#pending`, `#dropsSeen`, and `#reported`, whose + `#tryRecvGroup` closes any group already reported, become the same + per-cursor record. Untrusted input: a hostile upstream can fragment the set by alternating drops and arrivals, and an open live range is no bound, since its groups are @@ -116,14 +137,32 @@ per-run cost, and the same eviction bound in `js/net`. Under pressure the pool evicts the oldest runs as it evicts groups, and a run older than the cache's retention window is forgotten, so the set never outgrows what the pool would hold, with no new constant. An evicted run's sequences become -unknown again. A cursor judges an unknown missing sequence as it would a +unknown again. + +Each cursor's disposition record is billed the same way, a fixed per-run +cost on the same `Pool`, and is settled a run at a time like the shared +set, so a cursor's state and CPU track its runs, never their span. The pool +evicts a cursor's oldest run first: + +- An evicted reported run becomes unknown and is judged again from the + shared set and locally, as below. Nothing below it is recorded as + delivered, since older runs went first, so this never resends a delivered + group. A run reported over lite-05 or lite-06 was final on the wire, so + it is folded into the floor instead. +- An evicted delivered run raises the cursor's floor through it, as + `forget_below` does today: nothing below the floor is offered again, + which keeps a delivered group from going out twice at the cost of no + longer re-offering a gap the pool could not remember. + + A cursor judges an unknown missing sequence as it would a group never received: locally against its own budget (`TrackState::drifted`), and otherwise as a cache miss, which the relay fills upstream by range ([Subscribe ranges](/quest/m1/subscribe-ranges/README.md)), so no reader waits indefinitely on forgotten state. Extend `track_subscriber_churn` in `rs/moq-net/benches/track.rs` with a fragmented set up to pool capacity, -swept over subscriber count, run count, and run span, so enumeration cost -tracks the touched runs, never the set or a run's span. +swept over subscriber count, shared runs, runs per cursor record, and run +span, so enumeration cost tracks the touched runs, never the set, the +record, or a run's span. Test on mock time through a relay: - On a live track, a reader no wider than the upstream budget gets a prompt @@ -146,6 +185,14 @@ Test on mock time through a relay: - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. +- On lite-07, a cursor that delivered F, reported its neighbour G stale + while G stayed cached, and delivered H, gets only G after a widening that + covers all three: G is re-offered and F and H are never resent. The same + holds when G's convicting edge is lost instead of a widening. +- A cursor whose disposition record is evicted under pool pressure never + resends a delivered group: an evicted reported run is judged again, and + an evicted delivered run raises the floor. Through a lite-06 hop, an + evicted reported run stays dropped. - A flood of fragmented runs stays within the pool, evicting the oldest runs first, and a sequence whose run was evicted is judged locally or filled by range, never waited on indefinitely. From a13e573ea59db693c898587aaa0b41b3630f6d23 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 16:30:32 -0700 Subject: [PATCH 16/24] quest(m1): a cursor's floor never rises over an unknown hole Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 23843373d72..32ff8186a4c 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -141,20 +141,24 @@ unknown again. Each cursor's disposition record is billed the same way, a fixed per-run cost on the same `Pool`, and is settled a run at a time like the shared -set, so a cursor's state and CPU track its runs, never their span. The pool -evicts a cursor's oldest run first: - -- An evicted reported run becomes unknown and is judged again from the - shared set and locally, as below. Nothing below it is recorded as - delivered, since older runs went first, so this never resends a delivered - group. A run reported over lite-05 or lite-06 was final on the wire, so - it is folded into the floor instead. -- An evicted delivered run raises the cursor's floor through it, as - `forget_below` does today: nothing below the floor is offered again, - which keeps a delivered group from going out twice at the cost of no - longer re-offering a gap the pool could not remember. - - A cursor judges an unknown missing sequence as it would a +set, so a cursor's state and CPU track its runs, never their span. The +floor only rises over a settled prefix: the runs from the floor up to the +cursor's lowest unknown hole, each delivered or reported to the subscriber. +It never rises over a hole, since a descending or arrival-order cursor can +deliver H while a lower G is still unknown, and folding H would silently +treat G as handled. When the pool evicts a cursor's oldest run: + +- A run inside the settled prefix folds into the floor, as `forget_below` + does today. Nothing is lost: a delivered group is never sent again, and a + reported one was already reported, so folding it only forgoes re-offering + it after a widening or edge loss. +- A run above an unknown hole cannot be dropped without guessing, so the + cursor fails instead: the subscription ends with `TooFarBehind` (stream + code 0x5, already meaning a reader that fell too far behind and lost + content), and the subscriber re-subscribes. The cursor's whole record is + released with it. + +A cursor judges an unknown missing sequence as it would a group never received: locally against its own budget (`TrackState::drifted`), and otherwise as a cache miss, which the relay fills upstream by range ([Subscribe ranges](/quest/m1/subscribe-ranges/README.md)), so no reader @@ -189,10 +193,12 @@ Test on mock time through a relay: while G stayed cached, and delivered H, gets only G after a widening that covers all three: G is re-offered and F and H are never resent. The same holds when G's convicting edge is lost instead of a widening. -- A cursor whose disposition record is evicted under pool pressure never - resends a delivered group: an evicted reported run is judged again, and - an evicted delivered run raises the floor. Through a lite-06 hop, an - evicted reported run stays dropped. +- A cursor whose settled prefix is evicted under pool pressure folds it + into the floor and never resends a delivered group or re-offers a folded + reported one. +- A descending cursor that delivered H while a lower G is still unknown, + under pool pressure that evicts H's run, ends with `TooFarBehind` instead + of skipping G. - A flood of fragmented runs stays within the pool, evicting the oldest runs first, and a sequence whose run was evicted is judged locally or filled by range, never waited on indefinitely. From 4632c0a60107dcad56d91ab96ff3d6af08e30667 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 16:40:25 -0700 Subject: [PATCH 17/24] quest(m1): resolved-demand owns the cursor record, stale reports never fold Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 77 +++++++++++++------------------------ quest/m1/resolved-demand.md | 44 +++++++++++++++++++-- 2 files changed, 67 insertions(+), 54 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 32ff8186a4c..a4740fcc8b5 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -48,30 +48,21 @@ which keeps drops final for now. `TrackRun::poll_forward_drop` emits only recorded drops, cuts, or stale cached groups. A sequence the relay never cached is not judged until the track finishes. -- **Each cursor keeps its own disposition record,** next to the per-track - set, as the maintainer first framed it ("keep what was dropped within each - cursor and within the track state"). The shared set cannot hold it: a - group that arrives leaves the shared set, so it cannot remember that this - cursor reported cached G stale, and after a widening or edge loss the - cursor could not tell G (re-offer it) from a group it delivered (never - resend it). The record is one run-length map per cursor, each run marked - delivered or reported, a reported run carrying its reason, the budget it - was judged under, and for a local stale verdict the convicting edge. It - replaces today's `Delivered` (`rs/moq-net/src/model/delivered.rs`), whose - floor (`forget_below`) folds stale gaps in with delivered groups, and - absorbs `TrackRun::served` and JS `#reported`. Delivery must be recorded, - not inferred from the cursor position: an arrival-order or descending - cursor delivers out of sequence order, and #5929 re-offers below the - position, so only the delivered runs say what never goes out twice. The - record only answers "re-offer or not"; the serving and re-offer path stays - #5929's. +- **It extends resolved-demand's per-cursor disposition record,** which + stays next to the per-track set: a group that arrives leaves the shared + set, so only the cursor's record remembers that it reported cached G + stale. A reported run now carries its reason and, for a stale one, the + budget it was judged under. On lite-07 a reported stale run is no longer + final: like a skipped one, it is re-offered when a widening or edge loss + covers it. - **A widening re-offers the dropped runs now inside its budget,** through the re-offer path the resolved-demand quest adds, in the subscription's order. Upstream, the relay's own widening asks for them again. - **A local stale run that loses its edge is invalidated** as one span, the same as a widening invalidates it, matching resolved-demand's stale-edge - trigger. A run this cursor judged stale records, in its disposition - record, the newer group here that convicted it. When that group is + trigger. A run this cursor judged stale records, in resolved-demand's + disposition record, the newer group here that convicted it. When that + group is aborted or evicted, the run's sequences become unknown: a cached group is re-offered, and a missing one is judged again against the current edge (`TrackState::drifted`) or waited for. @@ -120,14 +111,9 @@ Code it replaces, in Rust and `js/net` alike: - `TrackState::drop_groups`, the `drops` range set and `DropCursor` in `rs/moq-net/src/model/track.rs`, which record a whole range without its reason or budget, even over groups already cached. -- `Delivered` in `rs/moq-net/src/model/delivered.rs` and - `TrackRun::served` in `rs/moq-net/src/lite/publisher.rs`, which hold - served and dropped sequences in one set and refuse any group in it. Both - become the per-cursor disposition record, which tells them apart. -- In `js/net/src/track.ts`: `#drop`, `#undrop`, and the `drops` signal go; - the wire cursor's `#pending`, `#dropsSeen`, and `#reported`, whose - `#tryRecvGroup` closes any group already reported, become the same - per-cursor record. +- In `js/net/src/track.ts`: `#drop`, `#undrop`, the `drops` signal, and + the wire cursor's `#pending` and `#dropsSeen`, whose `#tryRecvGroup` + closes any group already reported. Untrusted input: a hostile upstream can fragment the set by alternating drops and arrivals, and an open live range is no bound, since its groups are @@ -139,24 +125,14 @@ cache's retention window is forgotten, so the set never outgrows what the pool would hold, with no new constant. An evicted run's sequences become unknown again. -Each cursor's disposition record is billed the same way, a fixed per-run -cost on the same `Pool`, and is settled a run at a time like the shared -set, so a cursor's state and CPU track its runs, never their span. The -floor only rises over a settled prefix: the runs from the floor up to the -cursor's lowest unknown hole, each delivered or reported to the subscriber. -It never rises over a hole, since a descending or arrival-order cursor can -deliver H while a lower G is still unknown, and folding H would silently -treat G as handled. When the pool evicts a cursor's oldest run: - -- A run inside the settled prefix folds into the floor, as `forget_below` - does today. Nothing is lost: a delivered group is never sent again, and a - reported one was already reported, so folding it only forgoes re-offering - it after a widening or edge loss. -- A run above an unknown hole cannot be dropped without guessing, so the - cursor fails instead: the subscription ends with `TooFarBehind` (stream - code 0x5, already meaning a reader that fell too far behind and lost - content), and the subscriber re-subscribes. The cursor's whole record is - released with it. +The per-cursor record keeps resolved-demand's pool bound and floor rule, +with "final" narrowed: only a delivered run or a budget-independent drop (a +cut, a group never produced, or any drop reported over lite-05 or lite-06) +folds into the floor. A stale run reported on lite-07 within reach of the +max age bound is budget-qualified, so it is treated like an unknown hole: +the floor never rises over it, and if the pool must evict it or a run above +it, the cursor fails with `TooFarBehind`. Folding it would forgo the +re-offer this plan promises after a widening. A cursor judges an unknown missing sequence as it would a group never received: locally against its own budget (`TrackState::drifted`), @@ -193,12 +169,11 @@ Test on mock time through a relay: while G stayed cached, and delivered H, gets only G after a widening that covers all three: G is re-offered and F and H are never resent. The same holds when G's convicting edge is lost instead of a widening. -- A cursor whose settled prefix is evicted under pool pressure folds it - into the floor and never resends a delivered group or re-offers a folded - reported one. -- A descending cursor that delivered H while a lower G is still unknown, - under pool pressure that evicts H's run, ends with `TooFarBehind` instead - of skipping G. +- Under pool pressure, a cursor's delivered runs and budget-independent + drops fold into the floor, while evicting a lite-07 stale-reported run + within reach, or a run above it, ends the cursor with `TooFarBehind` + instead of losing the re-offer. Through a lite-06 hop the reported run + folds. - A flood of fragmented runs stays within the pool, evicting the oldest runs first, and a sequence whose run was evicted is judged locally or filled by range, never waited on indefinitely. diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 790a464f445..7d296cc9e5d 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -64,6 +64,38 @@ with no wire change. at the same max age when a stale verdict loses its edge: the newer group that made an older one stale is aborted or evicted, and `TrackState::is_stale` revalidates the edge, so a cold cursor would serve the older group. +- **Each cursor keeps a disposition record,** as the maintainer first framed + it ("keep what was dropped within each cursor and within the track + state"). It is what makes a re-offer safe: the cursor must tell a group it + gave up as stale (re-offer it) from one it delivered (never resend it). + The record is one run-length map per cursor, each run marked delivered, + skipped as stale (with the budget it was judged under and the newer group + here that convicted it, which the edge-loss trigger watches), or reported + in SUBSCRIBE_DROP (final for now, below). It replaces today's `Delivered` + (`rs/moq-net/src/model/delivered.rs`), whose floor (`forget_below`) folds + stale gaps in with delivered groups, and absorbs `TrackRun::served` in + `rs/moq-net/src/lite/publisher.rs` and the JS wire cursor's `#reported` in + `js/net/src/track.ts`. Delivery is recorded, not inferred from the cursor + position: an arrival-order or descending cursor delivers out of sequence + order, and #5929 re-offers below the position. The record only answers + "re-offer or not"; the serving and re-offer path stays #5929's. +- **The record is bounded by the pool, never by guessing.** Each run is + billed to the cache's `Pool` through a `Charge` at a fixed per-run cost + (the same bound in `js/net`) and is settled a run at a time, so a cursor's + state and CPU track its runs, never their span. The floor only rises over + a final prefix: the runs from the floor up to the lowest run that is not + final. Final means delivered, reported, or skipped as stale past the + track's max age bound, which no widening can reach since every budget is + clamped to it. A skipped stale run within reach is not final, and neither + is an unknown hole: a descending or arrival-order cursor can deliver H + while a lower G is still unknown, and folding H would silently treat G as + handled. When the pool evicts one of a cursor's runs: + - A run inside the final prefix folds into the floor, as `forget_below` + does today, and nothing is lost. + - Any other run cannot go without guessing, so the cursor fails: the + subscription ends with `TooFarBehind` (stream code 0x5, already meaning + a reader that fell too far behind and lost content), the subscriber + re-subscribes, and the cursor's whole record is released with it. - **Drops are final for now**, decided by the maintainer on 2026-10-19. A widening re-offers only groups this cursor never reported in SUBSCRIBE_DROP; a reported group stays dropped, as today. That leaves a @@ -87,7 +119,7 @@ Watch for: re-offered, not the cache, and make sure a reader joining with the same max age causes no scan and no SUBSCRIBE_UPDATE. Extend `track_subscriber_churn` in `rs/moq-net/benches/track.rs` with widening readers, swept over - subscriber count and cached groups. + subscriber count, cached groups, and runs per cursor record. - **`js/net` has the same bug and is in scope.** `combineSubscriptions` in `js/net/src/track.ts` already sends no floor when any subscriber has none, and its publisher needs the same re-offer on a widened max age. Land a JS @@ -103,8 +135,14 @@ shrink, evict, widen sequence at the relay gets the evicted groups back through the range miss fill, not a re-offer; and at the same max age, aborting or evicting the group that made an older one stale re-offers the older one, as a cold relay would serve it; and a stale group already reported in -SUBSCRIBE_DROP stays dropped after a widening, never served after its drop. -Cover the +SUBSCRIBE_DROP stays dropped after a widening, never served after its drop; +and a cursor that delivered F, skipped its neighbour G as stale while G +stayed cached, and delivered H, gets only G after a widening that covers +all three, or after G's convicting edge is lost, never F or H again; and a +cursor whose final prefix is evicted under pool pressure folds it and +resends nothing; and a descending cursor that delivered H while a lower G +is still unknown or skipped within reach, under pool pressure that evicts +H's run, ends with `TooFarBehind` instead of skipping G. Cover the warm-relay case in `js/net` too. Public API: none. Wire: no encoding change. A publisher now re-offers From d03dddb7d7edd93f5361e61b8b4000a123c7149f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 16:56:26 -0700 Subject: [PATCH 18/24] quest(m1): a stale drop names its edge, so a narrow range is not settled by it Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index a4740fcc8b5..e346b0721e3 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -29,8 +29,12 @@ which keeps drops final for now. - A run whose reason does not depend on the budget (a cut, a group never produced) is dropped for every reader as one range, as today. - A stale run is dropped promptly as one range for a reader whose budget - is no wider than the one it was judged under, and for any reader under - whose own budget `TrackState::drifted` convicts it. `drifted` judges a + is no wider than the one it was judged under and whose range reaches + the edge that convicted it, and for any reader under whose own budget + and range `TrackState::drifted` convicts it. A verdict depends on the + range as well as the budget, since `drifted` caps the edge to the + reader's range (`Edge::cap`): a reader whose range ends before the edge + would not call the group stale. `drifted` judges a missing sequence by its successor here, and every sequence in a missing run shares one successor, so one call settles the run. - A wider reader skips the run and waits for a group, a re-offer, or its @@ -66,14 +70,16 @@ which keeps drops final for now. aborted or evicted, the run's sequences become unknown: a cached group is re-offered, and a missing one is judged again against the current edge (`TrackState::drifted`) or waited for. -- **A received stale run records no edge.** Its groups are not held here, +- **A received stale run is never invalidated locally.** Its groups are not held here, so a cold cursor at this relay would fill them from upstream by range, and the upstream, still holding its edge, would drop them again. A local abort or eviction therefore never makes a received verdict obsolete; only the upstream changing its mind does, and it says so: on lite-07 it re-offers the group when its own edge is lost, and a widening re-asks. A received - run is invalidated only by an upstream re-offer or a widening. Cuts and - never-produced runs carry no edge and are unaffected. No wire field. + run is invalidated only by an upstream re-offer or a widening. The edge + sequence it carries (below) only checks a reader's range; no local event + watches it. Cuts and never-produced runs carry no edge and are + unaffected. - **A stale drop names its budget.** A SUBSCRIBE_UPDATE has no acknowledgement, so after a widening from B1 to B2, an in-flight B1 drop and the B2 re-drop look identical, and no guess settles a B2 reader. On @@ -89,6 +95,18 @@ which keeps drops final for now. and agreement on every update counted, while the budget itself is self-describing, order-free, and unaffected by updates that change nothing. +- **A stale drop names its edge.** Upstream convicts G with an edge E in + the aggregate range, which a narrower reader may not reach. Next to + `Max Age`, lite-07's stale SUBSCRIBE_DROP carries `Edge`, the sequence of + the group that convicted it (plus one, `0` when `Max Age` is `0`). A + cursor accepts the drop only when its own range cap reaches that + sequence, an O(1) check per run; otherwise the run is unknown to it and + it fills G by range, where the upstream judges G under that narrower + range. A relay forwards the field unchanged, since sequences are the + track's own. Rejected: each cursor confirming the drop with its own + capped `drifted`, which needs a successor of G held here, and a relay + that never cached G often holds none, so it would fall back to the range + fill anyway with more state to reason about. - **lite-07 only for re-offering reported groups.** Released lite-05 and lite-06 peers treat a flushed SUBSCRIBE_DROP as final, so a drop received over those versions is final for every reader, and a lite-07 hop never @@ -103,8 +121,8 @@ which keeps drops final for now. when it was sent, that a widening re-offers held groups and re-drops the rest, and that a later group wins over its drop. Whichever of this and [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - lands second writes it, including the `Max Age` field above, which is - free to add because lite-07 is unpublished. + lands second writes it, including the `Max Age` and `Edge` fields above, + which are free to add because lite-07 is unpublished. Code it replaces, in Rust and `js/net` alike: @@ -165,6 +183,10 @@ Test on mock time through a relay: - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. +- A reader whose range ends below the edge E that convicted G upstream is + not settled by the drop: it fills G by range and gets G or a drop judged + under its own range, while a reader whose range reaches E is settled at + once. - On lite-07, a cursor that delivered F, reported its neighbour G stale while G stayed cached, and delivered H, gets only G after a widening that covers all three: G is re-offered and F and H are never resent. The same @@ -184,7 +206,7 @@ Test on mock time through a relay: Mirror each case in `js/net`. Public API: none. Wire: lite-07 SUBSCRIBE_DROP (unpublished) gains `Max Age` -and the widening and ordering rules above; published versions are +and `Edge`, and the widening and ordering rules above; published versions are unchanged. ## Required From 0d673201ed45a804eef6f44d190b264c000c8acd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 17:03:43 -0700 Subject: [PATCH 19/24] quest(m1): only a widening reverses a stale verdict Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 100 +++++++++++++----------------------- quest/m1/resolved-demand.md | 37 +++++++------ 2 files changed, 59 insertions(+), 78 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index e346b0721e3..9d7bc875b16 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -28,15 +28,19 @@ which keeps drops final for now. in O(1): - A run whose reason does not depend on the budget (a cut, a group never produced) is dropped for every reader as one range, as today. - - A stale run is dropped promptly as one range for a reader whose budget - is no wider than the one it was judged under and whose range reaches - the edge that convicted it, and for any reader under whose own budget - and range `TrackState::drifted` convicts it. A verdict depends on the - range as well as the budget, since `drifted` caps the edge to the - reader's range (`Edge::cap`): a reader whose range ends before the edge - would not call the group stale. `drifted` judges a - missing sequence by its successor here, and every sequence in a missing - run shares one successor, so one call settles the run. + - A received stale run is dropped promptly as one range for a reader + whose budget is no wider than the one it was judged under and whose + range covers this relay's aggregate range for the track, which the + upstream judged under. A verdict depends on the range as well as the + budget, since `drifted` caps the edge to the reader's range + (`Edge::cap`), so a narrower reader treats the run as unknown and fills + it by range, where the upstream judges it under that narrower range. + The cost is an extra range request, only for narrower readers, when a + stale drop arrives. + - A missing run this relay judges itself is dropped for a reader under + whose own budget and range `TrackState::drifted` convicts it. `drifted` + judges a missing sequence by its successor here, and every sequence in + a missing run shares one successor, so one call settles the run. - A wider reader skips the run and waits for a group, a re-offer, or its own verdict, each of which splits the run at most once. - An unknown span (a gap between runs, or an evicted run) is judged the @@ -57,29 +61,16 @@ which keeps drops final for now. set, so only the cursor's record remembers that it reported cached G stale. A reported run now carries its reason and, for a stale one, the budget it was judged under. On lite-07 a reported stale run is no longer - final: like a skipped one, it is re-offered when a widening or edge loss - covers it. + final: like a skipped one, it is re-offered when a widening covers it. - **A widening re-offers the dropped runs now inside its budget,** through the re-offer path the resolved-demand quest adds, in the subscription's order. Upstream, the relay's own widening asks for them again. -- **A local stale run that loses its edge is invalidated** as one span, the - same as a widening invalidates it, matching resolved-demand's stale-edge - trigger. A run this cursor judged stale records, in resolved-demand's - disposition record, the newer group here that convicted it. When that - group is - aborted or evicted, the run's sequences - become unknown: a cached group is re-offered, and a missing one is judged - again against the current edge (`TrackState::drifted`) or waited for. -- **A received stale run is never invalidated locally.** Its groups are not held here, - so a cold cursor at this relay would fill them from upstream by range, and - the upstream, still holding its edge, would drop them again. A local abort - or eviction therefore never makes a received verdict obsolete; only the - upstream changing its mind does, and it says so: on lite-07 it re-offers - the group when its own edge is lost, and a widening re-asks. A received - run is invalidated only by an upstream re-offer or a widening. The edge - sequence it carries (below) only checks a reader's range; no local event - watches it. Cuts and never-produced runs carry no edge and are - unaffected. +- **Only a widening reverses a stale verdict,** as resolved-demand + decides, local or received alike: no run tracks the group that convicted + it, and an abort or eviction of that group leaves the run standing (the + open question in resolved-demand). A received run is also reversed by + the upstream re-offering its group after a widening, since a later group + wins over its drop. Cuts and never-produced runs are unaffected. - **A stale drop names its budget.** A SUBSCRIBE_UPDATE has no acknowledgement, so after a widening from B1 to B2, an in-flight B1 drop and the B2 re-drop look identical, and no guess settles a B2 reader. On @@ -95,18 +86,6 @@ which keeps drops final for now. and agreement on every update counted, while the budget itself is self-describing, order-free, and unaffected by updates that change nothing. -- **A stale drop names its edge.** Upstream convicts G with an edge E in - the aggregate range, which a narrower reader may not reach. Next to - `Max Age`, lite-07's stale SUBSCRIBE_DROP carries `Edge`, the sequence of - the group that convicted it (plus one, `0` when `Max Age` is `0`). A - cursor accepts the drop only when its own range cap reaches that - sequence, an O(1) check per run; otherwise the run is unknown to it and - it fills G by range, where the upstream judges G under that narrower - range. A relay forwards the field unchanged, since sequences are the - track's own. Rejected: each cursor confirming the drop with its own - capped `drifted`, which needs a successor of G held here, and a relay - that never cached G often holds none, so it would fall back to the range - fill anyway with more state to reason about. - **lite-07 only for re-offering reported groups.** Released lite-05 and lite-06 peers treat a flushed SUBSCRIBE_DROP as final, so a drop received over those versions is final for every reader, and a lite-07 hop never @@ -121,8 +100,8 @@ which keeps drops final for now. when it was sent, that a widening re-offers held groups and re-drops the rest, and that a later group wins over its drop. Whichever of this and [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) - lands second writes it, including the `Max Age` and `Edge` fields above, - which are free to add because lite-07 is unpublished. + lands second writes it, including the `Max Age` field above, which is + free to add because lite-07 is unpublished. Code it replaces, in Rust and `js/net` alike: @@ -152,7 +131,8 @@ the floor never rises over it, and if the pool must evict it or a run above it, the cursor fails with `TooFarBehind`. Folding it would forgo the re-offer this plan promises after a widening. -A cursor judges an unknown missing sequence as it would a +A cursor judges an unknown missing sequence (never a stale run its record +still holds as reported, which only a widening settles) as it would a group never received: locally against its own budget (`TrackState::drifted`), and otherwise as a cache miss, which the relay fills upstream by range ([Subscribe ranges](/quest/m1/subscribe-ranges/README.md)), so no reader @@ -165,40 +145,34 @@ record, or a run's span. Test on mock time through a relay: - On a live track, a reader no wider than the upstream budget gets a prompt drop for an upstream OLD naming a group the relay never cached. -- On lite-07, a widened reader gets that group re-offered and never the - drop, whether the drop was flushed before or after the widening - SUBSCRIBE_UPDATE. Check what each reader receives, not only tail - accounting. +- On lite-07, a widened reader gets that group re-offered. A drop already + flushed before the widening SUBSCRIBE_UPDATE was received, so the reader + then gets the re-offer and no further final drop; a drop still in flight + does not settle the widened reader. Check what each reader receives, not + only tail accounting. - On lite-07, after a widening from B1 to B2 for a group the upstream no longer holds, an in-flight drop tagged B1 settles only the B1 reader, and the re-drop tagged B2 settles the B2 reader. - A group arriving after its drop is served, and a drop arriving after its group leaves it served. -- At the same budget B, for a group G judged stale because of a newer - edge E: when the relay judged G itself and E is aborted or evicted there, - a B reader gets G re-offered from the cache. When G was reported stale by - the upstream and E is lost upstream, the upstream re-offers G, which wins - over the stored drop. When E is evicted only at the relay, the received - drop stands, as a cold relay filling G from upstream would also be told. - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. -- A reader whose range ends below the edge E that convicted G upstream is - not settled by the drop: it fills G by range and gets G or a drop judged - under its own range, while a reader whose range reaches E is settled at - once. +- A reader whose range is narrower than the relay's aggregate range is not + settled by a received stale drop: it fills G by range and gets G or a + drop judged under its own range, while a reader covering the aggregate + range is settled at once. - On lite-07, a cursor that delivered F, reported its neighbour G stale while G stayed cached, and delivered H, gets only G after a widening that - covers all three: G is re-offered and F and H are never resent. The same - holds when G's convicting edge is lost instead of a widening. + covers all three: G is re-offered and F and H are never resent. - Under pool pressure, a cursor's delivered runs and budget-independent drops fold into the floor, while evicting a lite-07 stale-reported run within reach, or a run above it, ends the cursor with `TooFarBehind` instead of losing the re-offer. Through a lite-06 hop the reported run folds. - A flood of fragmented runs stays within the pool, evicting the oldest - runs first, and a sequence whose run was evicted is judged locally or - filled by range, never waited on indefinitely. + shared runs first, and a sequence whose shared run was evicted is judged + locally or filled by range, never waited on indefinitely. - A single run spanning billions of sequences is settled by each cursor in one step, whether dropped, skipped by a wider reader, or evicted and judged again. @@ -206,7 +180,7 @@ Test on mock time through a relay: Mirror each case in `js/net`. Public API: none. Wire: lite-07 SUBSCRIBE_DROP (unpublished) gains `Max Age` -and `Edge`, and the widening and ordering rules above; published versions are +and the widening and ordering rules above; published versions are unchanged. ## Required diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 7d296cc9e5d..01b89463b04 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -60,18 +60,26 @@ with no wire change. nothing already served goes out twice; a group already delivered and since evicted downstream is a cache miss, which the relay fills upstream by range per [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) (a miss below - the cache becomes a range request), not a re-offer. The same re-offer fires - at the same max age when a stale verdict loses its edge: the newer group - that made an older one stale is aborted or evicted, and `TrackState::is_stale` - revalidates the edge, so a cold cursor would serve the older group. + the cache becomes a range request), not a re-offer. +- **Only a widening reverses a stale verdict,** a bigger budget or a wider + range, decided by the maintainer on 2026-10-19 ("Narrow, then merge"). + Nothing at the same terms re-offers a group the cursor gave up, so no + cursor tracks what convicted it. +- **Open question: warm/cold equivalence after an edge loss is not + promised.** When the newer group that made G stale is aborted or evicted, + a cold cursor may serve G while a warm one stays past it. Deferred + because watching for it needs more than this plan carries: a verdict + depends on two groups (`drifted` uses G's successor for `reach` and a + possibly later presentation edge in `live_above`, and losing either can + free G), a range may end mid-group so an edge needs a frame position, and + a run skipped at the max age bound would no longer be final. - **Each cursor keeps a disposition record,** as the maintainer first framed it ("keep what was dropped within each cursor and within the track state"). It is what makes a re-offer safe: the cursor must tell a group it gave up as stale (re-offer it) from one it delivered (never resend it). The record is one run-length map per cursor, each run marked delivered, - skipped as stale (with the budget it was judged under and the newer group - here that convicted it, which the edge-loss trigger watches), or reported - in SUBSCRIBE_DROP (final for now, below). It replaces today's `Delivered` + skipped as stale (with the budget and range it was judged under), or + reported in SUBSCRIBE_DROP (final for now, below). It replaces today's `Delivered` (`rs/moq-net/src/model/delivered.rs`), whose floor (`forget_below`) folds stale gaps in with delivered groups, and absorbs `TrackRun::served` in `rs/moq-net/src/lite/publisher.rs` and the JS wire cursor's `#reported` in @@ -84,9 +92,9 @@ with no wire change. (the same bound in `js/net`) and is settled a run at a time, so a cursor's state and CPU track its runs, never their span. The floor only rises over a final prefix: the runs from the floor up to the lowest run that is not - final. Final means delivered, reported, or skipped as stale past the - track's max age bound, which no widening can reach since every budget is - clamped to it. A skipped stale run within reach is not final, and neither + final. Final means delivered, reported, or skipped as stale at the + track's max age bound: only a widening reverses a verdict, and no + widening reaches past the bound since every budget is clamped to it. A skipped stale run within reach is not final, and neither is an unknown hole: a descending or arrival-order cursor can deliver H while a lower G is still unknown, and folding H would silently treat G as handled. When the pool evicts one of a cursor's runs: @@ -132,13 +140,12 @@ answer; a late group below the copy's start still reaches the no-floor reader; a reader joining with the same max age sends no SUBSCRIBE_UPDATE; and a re-offered group is never delivered twice to one subscriber; and a widen, shrink, evict, widen sequence at the relay gets the evicted groups back -through the range miss fill, not a re-offer; and at the same max age, aborting -or evicting the group that made an older one stale re-offers the older one, -as a cold relay would serve it; and a stale group already reported in +through the range miss fill, not a re-offer; and a stale group already reported in SUBSCRIBE_DROP stays dropped after a widening, never served after its drop; and a cursor that delivered F, skipped its neighbour G as stale while G stayed cached, and delivered H, gets only G after a widening that covers -all three, or after G's convicting edge is lost, never F or H again; and a +all three, through a bigger budget or a wider range, never F or H again; +and a run skipped at the max age bound folds into the floor; and a cursor whose final prefix is evicted under pool pressure folds it and resends nothing; and a descending cursor that delivered H while a lower G is still unknown or skipped within reach, under pool pressure that evicts @@ -147,7 +154,7 @@ warm-relay case in `js/net` too. Public API: none. Wire: no encoding change. A publisher now re-offers cached groups it never reported dropped when a SUBSCRIBE_UPDATE widens the -max age or a stale verdict loses its edge. +max age or the range. ## Required From 7e0cac19381874042d50f9c6584ac966e28bfdd7 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 17:09:38 -0700 Subject: [PATCH 20/24] quest(m1): defer in-flight stale drops across a range change Co-Authored-By: Claude Opus 5.5 --- quest/m1/resolved-demand.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 01b89463b04..c459f7eb013 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -72,7 +72,11 @@ with no wire change. depends on two groups (`drifted` uses G's successor for `reach` and a possibly later presentation edge in `live_above`, and losing either can free G), a range may end mid-group so an edge needs a frame position, and - a run skipped at the max age bound would no longer be final. + a run skipped at the max age bound would no longer be final. Likewise, + a stale drop in flight across an aggregate range change (lite-07, see + [Drop set](/quest/m1/drop-set.md)) may settle a reader whose own range + would not convict, since SUBSCRIBE_UPDATE has no acknowledgement and the + drop names only its `Max Age`; deferred for the same reason. - **Each cursor keeps a disposition record,** as the maintainer first framed it ("keep what was dropped within each cursor and within the track state"). It is what makes a re-offer safe: the cursor must tell a group it From 306f6a859e7d8c4c94d5ad1ba656ded91c52f88c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 17:14:29 -0700 Subject: [PATCH 21/24] quest(m1): a drop folded under pressure is final for its cursor Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 12 ++++++++---- quest/m1/resolved-demand.md | 4 +++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 9d7bc875b16..bceac514e30 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -98,7 +98,7 @@ which keeps drops final for now. - **Draft wording.** `drafts/draft-lcurley-moq-lite.md` states, for lite-07's restored SUBSCRIBE_DROP, that a drop holds only under the terms in effect when it was sent, that a widening re-offers held groups and re-drops the - rest, and that a later group wins over its drop. Whichever of this and + rest, and that a later group, when sent, wins over its drop. Whichever of this and [SUBSCRIBE_DROP accounts for every group](/quest/m1/subscribe-drop.md) lands second writes it, including the `Max Age` field above, which is free to add because lite-07 is unpublished. @@ -129,7 +129,10 @@ folds into the floor. A stale run reported on lite-07 within reach of the max age bound is budget-qualified, so it is treated like an unknown hole: the floor never rises over it, and if the pool must evict it or a run above it, the cursor fails with `TooFarBehind`. Folding it would forgo the -re-offer this plan promises after a widening. +re-offer this plan promises after a widening. A later group wins over its +drop only while the cursor's record still holds that drop: a drop folded +into the floor under pool pressure is final for that cursor, so its group +arriving late is not delivered there. A cursor judges an unknown missing sequence (never a stale run its record still holds as reported, which only a widening settles) as it would a @@ -153,8 +156,9 @@ Test on mock time through a relay: - On lite-07, after a widening from B1 to B2 for a group the upstream no longer holds, an in-flight drop tagged B1 settles only the B1 reader, and the re-drop tagged B2 settles the B2 reader. -- A group arriving after its drop is served, and a drop arriving after its - group leaves it served. +- A group arriving after its drop, while the cursor's record still holds + the drop, is served, and a drop arriving after its group leaves it + served. - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index c459f7eb013..49062ef9bda 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -76,7 +76,9 @@ with no wire change. a stale drop in flight across an aggregate range change (lite-07, see [Drop set](/quest/m1/drop-set.md)) may settle a reader whose own range would not convict, since SUBSCRIBE_UPDATE has no acknowledgement and the - drop names only its `Max Age`; deferred for the same reason. + drop names only its `Max Age`; deferred for the same reason. A group + arriving after its drop was folded into a cursor's floor under pool + pressure is not delivered to that cursor ([Drop set](/quest/m1/drop-set.md)). - **Each cursor keeps a disposition record,** as the maintainer first framed it ("keep what was dropped within each cursor and within the track state"). It is what makes a re-offer safe: the cursor must tell a group it From 9ea466ddd3132890f3bea06fc17f95ddb32b27e8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 17:20:16 -0700 Subject: [PATCH 22/24] quest(m1): eviction and expiry drops are budget-independent Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index bceac514e30..381d1156bec 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -20,14 +20,16 @@ which keeps drops final for now. - **One run-length set per track.** The track state keeps the dropped and expired group runs within the subscription range. Each run carries its - reason (stale, cut, never produced, evicted) and, for a stale run, the + reason (stale, cut, never produced, evicted or expired past retention) + and, for a stale run, the budget it was judged under. A group that arrives removes its sequence; a drop never removes a group that already arrived. - **Each cursor judges for itself, a run at a time.** A cursor walks the set's runs in its range, never their sequences, and settles each whole run in O(1): - A run whose reason does not depend on the budget (a cut, a group never - produced) is dropped for every reader as one range, as today. + produced, an eviction or retention expiry) is dropped for every reader + as one range, as today. - A received stale run is dropped promptly as one range for a reader whose budget is no wider than the one it was judged under and whose range covers this relay's aggregate range for the track, which the @@ -76,8 +78,13 @@ which keeps drops final for now. and the B2 re-drop look identical, and no guess settles a B2 reader. On lite-07 SUBSCRIBE_DROP carries `Max Age`: the budget a staleness verdict was judged under, encoded like `Publisher Max Age` (milliseconds plus one), - with `0` for a drop that does not depend on the budget (a cut, a group - never produced). The relay records each run with that exact budget. A B1 + with `0` for a drop that does not depend on the budget: a cut, a group + never produced, or a group evicted or expired past retention, none of + which depends on the reader's budget. A widening never reverses a `0` + drop; a reader that still wants an evicted group asks for it again with a + past-range request, as resolved-demand already does, and the upstream + fills it if it still holds it. The relay records each run with that + exact budget. A B1 drop then settles only readers no wider than B1, and the B2 re-drop settles a B2 reader. The cost is one varint per drop, a few bytes on an event that is already per group, which is worth an unambiguous term. @@ -124,7 +131,8 @@ unknown again. The per-cursor record keeps resolved-demand's pool bound and floor rule, with "final" narrowed: only a delivered run or a budget-independent drop (a -cut, a group never produced, or any drop reported over lite-05 or lite-06) +cut, a group never produced, an eviction or retention expiry, or any drop +reported over lite-05 or lite-06) folds into the floor. A stale run reported on lite-07 within reach of the max age bound is budget-qualified, so it is treated like an unknown hole: the floor never rises over it, and if the pool must evict it or a run above @@ -162,6 +170,10 @@ Test on mock time through a relay: - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. +- Rust and `js/net` each encode and decode an eviction drop and a + retention-expiry drop with `Max Age` `0`, across languages, and a + widening re-offers neither, while a past-range request still gets an + evicted group the upstream holds. - A reader whose range is narrower than the relay's aggregate range is not settled by a received stale drop: it fills G by range and gets G or a drop judged under its own range, while a reader covering the aggregate From 2885fee7fb8d6844dfd0249464c44ac4b83cdb5c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 18:00:14 -0700 Subject: [PATCH 23/24] quest(m1): a relay's own eviction is a range fill, never a drop Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 381d1156bec..0d1aec37e4d 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -20,15 +20,20 @@ which keeps drops final for now. - **One run-length set per track.** The track state keeps the dropped and expired group runs within the subscription range. Each run carries its - reason (stale, cut, never produced, evicted or expired past retention) - and, for a stale run, the + reason (stale, cut, never produced, or evicted or expired where no copy + remains: received as such from the upstream, or at an origin with no + upstream) and, for a stale run, the budget it was judged under. A group that arrives removes its sequence; a - drop never removes a group that already arrived. + drop never removes a group that already arrived. A relay's own eviction + is never a run: the group is unknown again, and any cursor that wants it, + new, widened, or already owed it, fills it from upstream by range, the + existing miss fill. That needs no new state, since the cursor's record + already holds the span as unknown, a run at a time. - **Each cursor judges for itself, a run at a time.** A cursor walks the set's runs in its range, never their sequences, and settles each whole run in O(1): - A run whose reason does not depend on the budget (a cut, a group never - produced, an eviction or retention expiry) is dropped for every reader + produced, an upstream or origin eviction or expiry) is dropped for every reader as one range, as today. - A received stale run is dropped promptly as one range for a reader whose budget is no wider than the one it was judged under and whose @@ -79,11 +84,11 @@ which keeps drops final for now. lite-07 SUBSCRIBE_DROP carries `Max Age`: the budget a staleness verdict was judged under, encoded like `Publisher Max Age` (milliseconds plus one), with `0` for a drop that does not depend on the budget: a cut, a group - never produced, or a group evicted or expired past retention, none of - which depends on the reader's budget. A widening never reverses a `0` - drop; a reader that still wants an evicted group asks for it again with a - past-range request, as resolved-demand already does, and the upstream - fills it if it still holds it. The relay records each run with that + never produced, or a group the sender evicted or expired with no copy it + could fetch (an origin, or a relay forwarding its upstream's eviction + drop), none of which depends on the reader's budget. A relay never sends + an eviction drop for its own eviction; it fills the group by range + instead. A widening never reverses a `0` drop. The relay records each run with that exact budget. A B1 drop then settles only readers no wider than B1, and the B2 re-drop settles a B2 reader. The cost is one varint per drop, a few bytes on an @@ -131,7 +136,7 @@ unknown again. The per-cursor record keeps resolved-demand's pool bound and floor rule, with "final" narrowed: only a delivered run or a budget-independent drop (a -cut, a group never produced, an eviction or retention expiry, or any drop +cut, a group never produced, an upstream or origin eviction or expiry, or any drop reported over lite-05 or lite-06) folds into the floor. A stale run reported on lite-07 within reach of the max age bound is budget-qualified, so it is treated like an unknown hole: @@ -170,10 +175,12 @@ Test on mock time through a relay: - Through a lite-06 hop, a reported group stays dropped for every reader, with no session error. - A shrink sends no drops. -- Rust and `js/net` each encode and decode an eviction drop and a - retention-expiry drop with `Max Age` `0`, across languages, and a - widening re-offers neither, while a past-range request still gets an - evicted group the upstream holds. +- Rust and `js/net` each encode and decode an origin's eviction drop and + retention-expiry drop with `Max Age` `0`, across languages; a relay + forwards them unchanged, and a widening re-offers neither. +- A group a relay evicted while the upstream still holds it is never + reported dropped: a new reader, a widened reader, and a reader already + owed it each fill it by range and receive it. - A reader whose range is narrower than the relay's aggregate range is not settled by a received stale drop: it fills G by range and gets G or a drop judged under its own range, while a reader covering the aggregate From 649134a83e847ac24b1cb4844693b33167b07b24 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 18:05:59 -0700 Subject: [PATCH 24/24] quest(m1): JS runs expire with the cache window, no new cap Co-Authored-By: Claude Opus 5.5 --- quest/m1/drop-set.md | 25 +++++++++++++++++-------- quest/m1/resolved-demand.md | 9 ++++++++- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 0d1aec37e4d..5c14778d898 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -128,11 +128,17 @@ Untrusted input: a hostile upstream can fragment the set by alternating drops and arrivals, and an open live range is no bound, since its groups are evicted while their runs would stay. Bill each run to the cache's existing byte accounting like a group: the Rust `Pool` through a `Charge` at a fixed -per-run cost, and the same eviction bound in `js/net`. Under pressure the -pool evicts the oldest runs as it evicts groups, and a run older than the -cache's retention window is forgotten, so the set never outgrows what the -pool would hold, with no new constant. An evicted run's sequences become -unknown again. +per-run cost. Under pressure the pool evicts the oldest runs as it evicts +groups, and a run older than the cache's retention window is forgotten, so +the set never outgrows what the pool would hold, with no new constant. An +evicted run's sequences become unknown again. `js/net` has no byte pool and +gets no new cap: its shared and per-cursor runs expire with the same idle +window as the groups they sit between, and expiry applies the same +fold-or-`TooFarBehind` rule as a Rust eviction. A run sits between cached +sequences, so there are at most about one per cached sequence (alternating +drop and arrival), and JS run state grows no faster than the JS cache +itself. The JS byte bound for both is +[JS unbounded groups](/quest/m2/unbounded-groups/js.md). The per-cursor record keeps resolved-demand's pool bound and floor rule, with "final" narrowed: only a delivered run or a budget-independent drop (a @@ -193,9 +199,11 @@ Test on mock time through a relay: within reach, or a run above it, ends the cursor with `TooFarBehind` instead of losing the re-offer. Through a lite-06 hop the reported run folds. -- A flood of fragmented runs stays within the pool, evicting the oldest - shared runs first, and a sequence whose shared run was evicted is judged - locally or filled by range, never waited on indefinitely. +- A flood of fragmented runs stays within the pool in Rust, evicting the + oldest shared runs first, and a sequence whose shared run was evicted is + judged locally or filled by range, never waited on indefinitely. In + `js/net` the same flood holds at most one run more than its cached + sequences, and the runs expire with the idle window. - A single run spanning billions of sequences is settled by each cursor in one step, whether dropped, skipped by a wider reader, or evicted and judged again. @@ -214,3 +222,4 @@ unchanged. ## Related - [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - fills the cache miss an evicted run leaves +- [JS unbounded groups](/quest/m2/unbounded-groups/js.md) - the JS byte bound for groups and the runs between them; not required, since JS runs never outgrow the JS cache diff --git a/quest/m1/resolved-demand.md b/quest/m1/resolved-demand.md index 49062ef9bda..a00ee625d33 100644 --- a/quest/m1/resolved-demand.md +++ b/quest/m1/resolved-demand.md @@ -95,7 +95,7 @@ with no wire change. "re-offer or not"; the serving and re-offer path stays #5929's. - **The record is bounded by the pool, never by guessing.** Each run is billed to the cache's `Pool` through a `Charge` at a fixed per-run cost - (the same bound in `js/net`) and is settled a run at a time, so a cursor's + and is settled a run at a time, so a cursor's state and CPU track its runs, never their span. The floor only rises over a final prefix: the runs from the floor up to the lowest run that is not final. Final means delivered, reported, or skipped as stale at the @@ -110,6 +110,12 @@ with no wire change. subscription ends with `TooFarBehind` (stream code 0x5, already meaning a reader that fell too far behind and lost content), the subscriber re-subscribes, and the cursor's whole record is released with it. + + `js/net` has no byte pool and gets no new cap: a cursor's runs expire + with the same idle window as the groups they sit between, under the same + fold-or-`TooFarBehind` rule, so there are at most about one per cached + sequence and the record grows no faster than the JS cache. The JS byte + bound is [JS unbounded groups](/quest/m2/unbounded-groups/js.md). - **Drops are final for now**, decided by the maintainer on 2026-10-19. A widening re-offers only groups this cursor never reported in SUBSCRIBE_DROP; a reported group stays dropped, as today. That leaves a @@ -170,3 +176,4 @@ max age or the range. - [Archive producer](/quest/m1/archive/producer.md) - its resume floor crosses relays that viewers may have warmed - [Drop set](/quest/m1/drop-set.md) - closes the gap this leaves for groups already reported dropped +- [JS unbounded groups](/quest/m2/unbounded-groups/js.md) - the JS byte bound for groups and the cursor runs between them