From 1ad01fa06cf163c2bb00c5c149e425f236f1078d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 2 Nov 2026 21:01:41 -0800 Subject: [PATCH] quest: import #7573, #7579, #7646 and settle time-addressing's timestamps Plans the three open issues and records the /quest-import grill that reshaped time-addressing: every write takes a timestamp (no wall-clock stamping), legacy group IDs pass through verbatim as keys, scale-less tracks keep lite-07's Timescale 0, and moq-transport drops our Timestamp property while keeping Prior Group ID Gap as a best-effort drop signal. New m1 quests: JSON writes take a broadcast-clock timestamp (#7573), catalog delay is the minimum trail (#7579), a lapsed request token resets with EXPIRED_AUTH_TOKEN (#7646 point 1), and @moq/net group IDs past 2^53. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 4 ++ quest/m1/auth-expiry-reset.md | 35 +++++++++++++ quest/m1/catalog-delay-min.md | 46 +++++++++++++++++ quest/m1/js-group-u62.md | 28 +++++++++++ quest/m1/json-required-timestamp.md | 49 ++++++++++++++++++ quest/m1/max-age-defaults.md | 2 +- quest/m1/time-addressing/README.md | 2 +- quest/m1/time-addressing/bindings.md | 9 ++-- quest/m1/time-addressing/ietf.md | 74 +++++++++++++++++----------- quest/m1/time-addressing/js.md | 5 +- quest/m1/time-addressing/lite07.md | 11 +++-- quest/m1/time-addressing/model.md | 73 ++++++++++++++++++++++----- 12 files changed, 284 insertions(+), 54 deletions(-) create mode 100644 quest/m1/auth-expiry-reset.md create mode 100644 quest/m1/catalog-delay-min.md create mode 100644 quest/m1/js-group-u62.md create mode 100644 quest/m1/json-required-timestamp.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 2301f2b5d3e..4ac7931ae00 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -133,12 +133,15 @@ per-frame CPU growth since #5926. ## Required - [Address groups by timestamp](/quest/m1/time-addressing/README.md) - group start timestamps replace sequence numbers in the moq-net API and lite-07 wire, while old subscribers, old publishers, and published JS clients keep working +- [@moq/net group IDs past 2^53](/quest/m1/js-group-u62.md) - JS carries every 62-bit group ID instead of refusing those past 2^53 - [Pre-cut audit findings](/quest/m1/cut-audit/README.md) - fix the crashes, hostile-input costs, and correctness bugs the 2026-10-24 audit of main found - [Vendored code has every upstream fix it needs](/quest/m1/vendor-sync/README.md) - quinn, libv4l-rs, nvidia-video-codec-sdk, rust-media-libs, cros-codecs, and UniFFI generator fixes are ported or planned, once per release cycle - [Mesh dials recover from a stuck flow](/quest/m1/mesh-fresh-port.md) - a relay redials a failing cluster peer from a fresh UDP port, so one dropped 5-tuple can't strand a link - [Cluster routing](/quest/m1/cluster-routing/README.md) - prove shared routing contracts, implement per-origin routes, and connect outbound local clusters - [Routing guide](/quest/m1/routing-guide.md) - concepts and practical outbound federation setup - [moq-c at parity](/quest/m1/c/README.md) - the hand-written `moq-c` ships at the cut with every m1 binding feature moq-ffi gains +- [JSON writes take a broadcast-clock timestamp](/quest/m1/json-required-timestamp.md) - every binding's JSON write requires one, moq-c's NULL fails, and C gains `moq_publish_catalog_now()`, so C media stops advertising an 8 s delay +- [Catalog delay is the least a track trails](/quest/m1/catalog-delay-min.md) - the estimator keeps the minimum trail, so B-frame reorder never becomes `delay` - [Bindings see a moq-transport run end as an end](/quest/m1/ffi-ietf-run-end.md) - an IETF publisher ending its run reaches moq-ffi consumers as an end, not `Internal` - [Byte-stream renditions form a ladder](/quest/m1/stream-ladder.md) - moq-ffi stream producers link as `lower` and disable unfundable rungs like frame producers - [A job's exact route continues its claim's readers](/quest/m1/announce-takeover.md) - a job's route inherits its claim's instance, replacing the ANNOUNCE_TAKEOVER predecessor design @@ -157,6 +160,7 @@ per-frame CPU growth since #5926. - [moq-crypto replaces moq-sign and moq-e2ee](/quest/m1/moq-crypto.md) - one Rust crate and one JS package hold signing and end-to-end encryption, sharing moq-net's timestamp check - [moq-transport 18, 20, and 22 only](/quest/m1/ietf-drop/README.md) - drafts 14-17, 19, and 21 are deleted from Rust, JS, tests, interop lanes, and docs, removing the legacy control-stream adapter and its request limit - [A moq-transport request token is one auth per distinct token](/quest/m1/ietf-token-auth.md) - requests naming the same token share one verification and grant, on any request type, ending with the last such request +- [A lapsed request token resets with EXPIRED_AUTH_TOKEN](/quest/m1/auth-expiry-reset.md) - group streams of a lapsed grant reset with the expiry code, so a client knows to renew - [moq-quic's API improves on quinn's](/quest/m1/quic-api/README.md) - moq-quic and both QUIC drivers ship one mirrored API: `send::`/`recv::` namespaces, one error per direction, consuming terminal operations, plain configs, no quinn baggage - [On-demand JavaScript FETCH](/quest/m1/js-fetch-on-demand.md) - browser publishers answer IETF FETCH for evicted or uncached groups through the JS ranges request surface - [Native rung disable](/quest/m1/simulcast-rung-disable.md) - OBS stops sending the top renditions its grant cannot fund, as the Rust/FFI publishers do, and a shaped uplink through moq-ffi proves the resume diff --git a/quest/m1/auth-expiry-reset.md b/quest/m1/auth-expiry-reset.md new file mode 100644 index 00000000000..98935af95b7 --- /dev/null +++ b/quest/m1/auth-expiry-reset.md @@ -0,0 +1,35 @@ +# [XS] A lapsed request token resets its groups with EXPIRED_AUTH_TOKEN + +## Goal + +When a moq-transport request token's grant lapses, the group streams of the +subscriptions it admitted reset with the draft's EXPIRED_AUTH_TOKEN code +instead of INTERNAL_ERROR. A client can then tell "renew and retry" from +"denied" without parsing PUBLISH_DONE's reason phrase. + +## Plan + +Point 1 of [moq-dev/moq#7646](https://github.com/moq-dev/moq/issues/7646) +(ksletmoe-aws), verified on `main`. A lapsed grant ends the subscription with +PUBLISH_DONE status UNAUTHORIZED and reason "token expired" +(`Served::status` in `rs/moq-net/src/ietf/publisher.rs`, which notes that +PUBLISH_DONE has no token-expiry status), and its group streams reset with +INTERNAL_ERROR. + +Decided 2026-11-03 (maintainer, `/quest-import`): ✅ reset with +EXPIRED_AUTH_TOKEN; no wire change (the next request's REQUEST_ERROR already +says EXPIRED_AUTH_TOKEN); won't fix. PUBLISH_DONE keeps UNAUTHORIZED, since +its own EXPIRED means the subscription's lifetime ran out. + +Check the reset code in drafts 18, 20, and 22. The report cites 0x07 for a +stream reset, while 0x5 is the request error code moq-net already sends +(`rs/moq-net/src/ietf/error.rs`). Use what each draft defines for stream +resets. Test on mocked time that a lapsed grant resets its groups with that +code and a revoked one doesn't, and note it in `doc/bin/relay/auth.md`. + +Public API: none. Wire: moq-transport group resets on token lapse change +code. + +## Related + +- [moq-transport request tokens](/quest/m1/ietf-token-auth.md) - #7646's points 2 to 4 land there diff --git a/quest/m1/catalog-delay-min.md b/quest/m1/catalog-delay-min.md new file mode 100644 index 00000000000..31d86c974b7 --- /dev/null +++ b/quest/m1/catalog-delay-min.md @@ -0,0 +1,46 @@ +# [S] A track's catalog delay is the least it trails the others + +## Goal + +The catalog estimator's `delay` is the smallest amount a track's frames have +trailed the highest timestamp another track wrote, not the largest. A +constant offset between tracks still shows as `delay`. B-frame reorder, which +only some frames trail by, never does: not on the B-frame track, not on a +sibling rendition, and not on the audio beside it. + +## Plan + +Reported in [moq-dev/moq#7579](https://github.com/moq-dev/moq/issues/7579) +and verified on `main`. `Order` (`rs/moq-mux/src/catalog/estimate.rs`) +compares presentation timestamps written in decode order, and +`Estimator::order` keeps the maximum trail as `delay`. A B-frame track's high +runs ahead of the frames it writes, so every track written beside it +advertises the reorder as `delay` (40 to 50 ms in the report). The player +then counts it twice, because the measured arrival lateness already holds it: +130 ms of playout against 82 ms on libmoq 0.6.17. + +Decided 2026-11-03 (maintainer, `/quest-import`): + +- Fix: ✅ `delay` is the minimum trail; subtract each track's declared + reorder (containers only); something else. The reporter's prototype (decode + edges, with the reorder taken from the SPS depth through `DeclaredClock`) + was rejected: "I definitely don't want SPS depth". +- Why the minimum: `delay` exists for a constant offset between tracks, which + cancels out of the receiver's `measured` (`doc/concept/audio-jitter.md`, + "Across renditions"). Reference pictures, and the audio of the same decode + slot, trail by zero, so the minimum drops the reorder. A genuinely late + track, every frame of which trails, keeps its offset. Variable lateness is + left to `jitter` and the receiver's measurement, since playout is + `delay + max(measured, jitter)`. + +A track that has never trailed keeps no `delay`. Keep the reporter's lockstep +test (two 50 fps renditions with two B-frames, plus audio, in decode order) +and assert no track gets a `delay`, with and without a declared reorder. Keep +the existing late-track tests. Update `Estimate::delay`'s doc and the `Order` +comment. + +Public API: none. Wire: catalog `delay` values shrink for B-frame broadcasts. + +## Closes + +- [#7579](https://github.com/moq-dev/moq/issues/7579) - catalog `delay` counts B-frame reorder diff --git a/quest/m1/js-group-u62.md b/quest/m1/js-group-u62.md new file mode 100644 index 00000000000..5881d4a2486 --- /dev/null +++ b/quest/m1/js-group-u62.md @@ -0,0 +1,28 @@ +# [S] @moq/net group IDs past 2^53 + +## Goal + +`@moq/net` decodes, holds, and re-encodes every group ID the wire allows (a +62-bit varint) on lite and moq-transport, instead of refusing any past 2^53. +A track whose group IDs are large, such as start ticks on a fine timescale +or a third-party moq-transport publisher's IDs, plays and relays through JS. + +## Plan + +Decided 2026-11-03 (maintainer, the `/quest-import` grill of +[moq-dev/moq#7573](https://github.com/moq-dev/moq/issues/7573)): "JS should +fix that regardless". Legacy group IDs now pass through verbatim as keys +([the model](/quest/m1/time-addressing/model.md)), so JS can no longer assume +they stay small. This replaces +[@moq/net by start](/quest/m1/time-addressing/js.md)'s plan to refuse a start +past 2^53. + +Today `js/net/src/lite/group.ts` reads and writes the sequence with `u53`, +and the moq-transport codecs do the same. Carry group IDs as `bigint` at the +wire and in the model, or add a checked u62 type if `bigint` costs too much +on the hot path. Measure both with the JS benches and pick the simpler one +that holds. The API breaks where a group ID is a `number` today. Test the +decode and encode of 2^53 + 1 and 2^62 - 1 on every wire version, through a +JS relay hop. + +Public API: breaking in `@moq/net` where group IDs are numbers. Wire: none. diff --git a/quest/m1/json-required-timestamp.md b/quest/m1/json-required-timestamp.md new file mode 100644 index 00000000000..5dc8e02f4dc --- /dev/null +++ b/quest/m1/json-required-timestamp.md @@ -0,0 +1,49 @@ +# [S] JSON writes take a broadcast-clock timestamp in every binding + +## Goal + +Every JSON snapshot update and stream append takes a required timestamp on +the broadcast clock: `moq_mux::json`'s `Snapshot` and `Stream`, moq-ffi's +JSON producers, and moq-c's `moq_publish_json_snapshot_update` and +`moq_publish_json_stream_append`, where a NULL timestamp fails instead of +being stamped with `now()`. moq-c gains `moq_publish_catalog_now()`, so a C +publisher stamps its media and its data in the broadcast clock's epoch. A C +publisher's JSON tracks no longer give its media an 8 s catalog `delay`. + +## Plan + +Reported in [moq-dev/moq#7573](https://github.com/moq-dev/moq/issues/7573) +and verified on `main`. moq-c stamps a NULL-timestamp JSON write with the +catalog clock's `now()` (`json_snapshot_update` and `json_stream_append` in +`rs/moq-c/src/publish.rs`). That write takes a seat in the catalog's write +`Order` against C media on the publisher's own timeline, so every media +track advertises the gap between the two timelines, about 8 s, as `delay`. +Rust and moq-ffi publish a null timestamp untimed instead, and moq-ffi +already exposes `MoqMediaCatalogProducer::now()`. + +Decided 2026-11-03 (maintainer, `/quest-import`): + +- Fix: require a timestamp, and expose `now()` to C, "because every track in + a broadcast needs to use the same epoch". Options were: NULL writes + unordered; ✅ require a timestamp (with C `now()`); won't fix. +- Scope: ✅ every binding in one quest; moq-c only; moq-c now and the others + later. +- This is the JSON part of + [time-addressing's required timestamps](/quest/m1/time-addressing/model.md), + which removes untimed writes everywhere. It lands first because #7573 is a + live bug. + +`moq_publish_catalog_now(broadcast, moq_timestamp *dst)` mirrors moq-ffi's +`now()`. `doc/lib/c` tells a C publisher to stamp media and timed data from +it. A NULL timestamp returns the invalid-argument error and publishes +nothing. Test that a C broadcast with JSON tracks advertises no `delay` on +its media, and that NULL fails, and update the moq-ffi and Rust JSON tests to +pass a timestamp. + +Public API: breaking in moq-c (NULL refused, `moq_publish_catalog_now` +added), moq-ffi (JSON `timestamp` required), and `moq_mux::json` (a write +takes a timestamp). Wire: none. + +## Closes + +- [#7573](https://github.com/moq-dev/moq/issues/7573) - moq-c JSON writes without a timestamp give every media track ~8 s of delay diff --git a/quest/m1/max-age-defaults.md b/quest/m1/max-age-defaults.md index 091579dfa29..de7f827b18b 100644 --- a/quest/m1/max-age-defaults.md +++ b/quest/m1/max-age-defaults.md @@ -28,7 +28,7 @@ Before claiming, checked ready state with `quest unblocked` and searched open PR ### Current defaults -Publisher max-age is a freshness bound on **superseded groups**. A group expires when either its successor's arrival is old enough at this hop or the successor's first timestamp trails the live edge enough. Untimed tracks use the wall clock. The newest group is exempt. Byte reclamation has a separate cache policy. See [rs/moq-net/src/model/track.rs:120](https://github.com/moq-dev/moq/blob/c95a9b1201/rs/moq-net/src/model/track.rs#L120) and [js/net/src/track.ts:137](https://github.com/moq-dev/moq/blob/c95a9b1201/js/net/src/track.ts#L137). +Publisher max-age is a freshness bound on **superseded groups**. A group expires when either its successor's arrival is old enough at this hop or the successor's first timestamp trails the live edge enough. Untimed tracks use the wall clock today; once [time-addressing](/quest/m1/time-addressing/model.md) lands, a scale-less track counts max-age in bare key units instead (decided 2026-11-03). The newest group is exempt. Byte reclamation has a separate cache policy. See [rs/moq-net/src/model/track.rs:120](https://github.com/moq-dev/moq/blob/c95a9b1201/rs/moq-net/src/model/track.rs#L120) and [js/net/src/track.ts:137](https://github.com/moq-dev/moq/blob/c95a9b1201/js/net/src/track.ts#L137). | Surface | Current default and meaning | Source | | --- | --- | --- | diff --git a/quest/m1/time-addressing/README.md b/quest/m1/time-addressing/README.md index b6a8d73e891..d358f3f110a 100644 --- a/quest/m1/time-addressing/README.md +++ b/quest/m1/time-addressing/README.md @@ -13,7 +13,7 @@ compatibility. - [Rust groups by start](/quest/m1/time-addressing/model.md) - the moq-net model and API address groups by start, and legacy wires carry the start ticks as the group ID - [Lite-07 by start](/quest/m1/time-addressing/lite07.md) - lite-07 carries starts, previous-start links, GROUP_END, and time bounds instead of sequence numbers -- [moq-transport starts and ends](/quest/m1/time-addressing/ietf.md) - Prior Group ID Gap and TIMESTAMP carry links and PTS, END_OF_GROUP marks a whole group, and the end time needs an extension +- [moq-transport IDs verbatim](/quest/m1/time-addressing/ietf.md) - group IDs pass through as keys, Prior Group ID Gap signals drops best-effort, END_OF_GROUP marks a whole group, and our Timestamp property goes - [@moq/net by start](/quest/m1/time-addressing/js.md) - JS matches Rust's model, wire, and legacy mapping - [Consumers by start](/quest/m1/time-addressing/consumers.md) - hang drops duration and discontinuity markers, and every consumer that numbered groups uses starts - [Bindings by start](/quest/m1/time-addressing/bindings.md) - moq-ffi, moq-c, and every binding expose starts, with migration docs diff --git a/quest/m1/time-addressing/bindings.md b/quest/m1/time-addressing/bindings.md index e86a0684aa2..9a702667fa6 100644 --- a/quest/m1/time-addressing/bindings.md +++ b/quest/m1/time-addressing/bindings.md @@ -10,9 +10,12 @@ failover, with each `doc/lib` page and the PR's Impact section saying how to mig Follow the cross-package sync table from `rs/moq-ffi` outward. Timestamps already cross the bindings as `{value, scale}`, so a start reuses that -type. Decided 2026-10-27 (maintainer): every track is timed after the cut, -so reads drop the `Option` that exact timestamps keeps for untimed. Only -reads drop it here; producer arguments keep their optional timestamp. +type. Decided 2026-11-03 (maintainer, replacing 2026-10-27's "producer arguments +keep their optional timestamp"): every read and write carries a timestamp. +Reads drop the `Option` that exact timestamps keeps for untimed, producer +arguments take a required one, and nothing is wall-clock stamped. A +scale-less track's frames carry their group's key. See +[the model](/quest/m1/time-addressing/model.md). Public API: breaking in every binding. Wire: none. diff --git a/quest/m1/time-addressing/ietf.md b/quest/m1/time-addressing/ietf.md index fbfb7d8afde..572cfec285e 100644 --- a/quest/m1/time-addressing/ietf.md +++ b/quest/m1/time-addressing/ietf.md @@ -1,39 +1,53 @@ -# [M] moq-transport carries group starts and ends +# [S] moq-transport carries group IDs verbatim and gaps best-effort ## Goal -A moq-transport peer that understands the MOQ Properties sees each group's -predecessor link, each object's PTS, and each group's end, and moq-net reads -them back on ingest. A peer without them still plays live; only end -precision and gap detection degrade. +moq-net keeps a moq-transport group ID verbatim as the group's key in both +directions. It sends each group's predecessor gap in Prior Group ID Gap as a +best-effort drop signal, and marks a whole group with END_OF_GROUP. It sends +no Timestamp object property of its own. LOC object properties are proxied +only between two moq-transport peers. ## Plan -Decided 2026-10-25 (maintainer), from the spike: - -- The Group ID is the start ticks ([the model](/quest/m1/time-addressing/model.md) - owns that mapping). The previous-start link rides in the Prior Group ID Gap - property. -- Object IDs stay +1 per frame, so no Prior Object ID Gap is sent. Each - object's PTS rides in TIMESTAMP (0x10), with TIMESCALE (0x08) on the track, - the MOQ Properties shared with draft-ietf-moq-loc, so it stays LOC - compatible. -- GROUP_END maps to END_OF_GROUP. The supported drafts refuse properties on - a status object (`check_status` in `rs/moq-net/src/ietf/object.rs`), so - END_OF_GROUP stays bare and the end needs a negotiated extension or is - lost, as the extension rule below allows. -- Rejected: Object ID as a PTS delta, because B-frames run backwards in - decode order and open-GOP leading frames present before the group's start. -- The properties are optional on both sides: moq-transport may carry them as - properties or a negotiated extension, and a peer sending none still works - for live playback. - -Test both directions over drafts 18 and 22, with and without the properties. - -Public API: none. Wire: moq-transport Prior Group ID Gap and TIMESTAMP are -sent and read, END_OF_GROUP marks a whole group, and the end time rides a -negotiated extension if one is defined. +Decided 2026-11-03 (maintainer, the `/quest-import` grill of +[moq-dev/moq#7573](https://github.com/moq-dev/moq/issues/7573)), replacing +the 2026-10-25 plan that carried each object's PTS in TIMESTAMP (0x10) with +TIMESCALE (0x08): + +- **The Group ID is the key**, verbatim on ingest and egress + ([the model](/quest/m1/time-addressing/model.md) owns that mapping). A + moq-transport track is in whatever unit its IDs are in; nothing infers + time from its objects. +- **No Timestamp property from us.** moq-net stops sending TIMESTAMP on + egress and doesn't lift it into the frame on ingest. LOC object properties + pass through unchanged when both sides speak moq-transport, and are dropped + toward moq-lite. A moq-transport publisher's LOC media relayed to a + moq-lite viewer therefore arrives without a frame PTS, which is accepted. + Today's TIMESTAMP code (`ietf/group.rs`, `ietf/object.rs`, + `ietf/max_age.rs`) is deleted or reduced to that pass-through. +- **Prior Group ID Gap stays** as a best-effort SUBSCRIBE_DROP signal: + egress sends the gap to the previous key, and ingest reads it as the + skipped IDs being dropped. A peer that sends none still works. +- **GROUP_END maps to END_OF_GROUP.** The supported drafts refuse properties + on a status object (`check_status` in `rs/moq-net/src/ietf/object.rs`), so + END_OF_GROUP stays bare and the end time is lost on this wire. +- Object IDs stay +1 per frame, so no Prior Object ID Gap is sent. + +Decision prompts: + +- Timestamp property: ✅ stop, with LOC proxied only between moq-transport + peers; keep reading but stop sending; keep the 2026-10-25 plan. +- Prior Group ID Gap: drop it; ✅ keep it as a best-effort SUBSCRIBE_DROP + signal. + +Test both directions over drafts 18, 20, and 22: IDs round-trip exactly +through a relay, a gap reaches the subscriber as a drop, and LOC properties +survive moq-transport to moq-transport. + +Public API: none. Wire: moq-transport loses our TIMESTAMP property, and +gains Prior Group ID Gap and END_OF_GROUP on whole groups. ## Required -- [Rust groups by start](/quest/m1/time-addressing/model.md) - the model whose links and ends these carry +- [Rust groups by start](/quest/m1/time-addressing/model.md) - the model whose keys and gaps these carry diff --git a/quest/m1/time-addressing/js.md b/quest/m1/time-addressing/js.md index c3fcb646510..567820a5db7 100644 --- a/quest/m1/time-addressing/js.md +++ b/quest/m1/time-addressing/js.md @@ -12,8 +12,9 @@ Mirror [the model](/quest/m1/time-addressing/model.md) and increasing starts, the three group ends, reach from a group's own end, and `Relative(n)` counting groups. The gap machinery that assumes `sequence + 1` (`delivered.ts`, `ledger.ts`, `track.ts`, `resume.ts`) follows -previous-start links instead. Starts stay JavaScript numbers, so a decoded -start past 2^53 is refused as today's group IDs are. +previous-start links instead. Starts are carried past 2^53, as +[@moq/net group IDs past 2^53](/quest/m1/js-group-u62.md) does for today's +group IDs (decided 2026-11-03, maintainer). The spike found no sparse-ID breakage in direct `@moq/net` sessions over lite-06 and lite-07; its relay fills follow Rust's once diff --git a/quest/m1/time-addressing/lite07.md b/quest/m1/time-addressing/lite07.md index 4bf6f3ced2f..28eabec2dca 100644 --- a/quest/m1/time-addressing/lite07.md +++ b/quest/m1/time-addressing/lite07.md @@ -26,10 +26,13 @@ Decided 2026-10-25 (maintainer), from the spike: - Positions stay (start, frame index), preserving transport frame offsets. - SUBSCRIBE_UPDATE's `Wanted Group` (a sequence plus one) and SUBSCRIBE_OK's `Group` and `Largest Group` are sequences too, and change with the rest. -- Every track is timed in the [model](/quest/m1/time-addressing/model.md), - so lite-07's `Timescale` 0 encoding goes: FRAME and datagrams always carry - a timestamp, and the untimed cases of TIMESTAMP_MISMATCH, Time anchors, - and arrival-age-only expiry leave the draft. The rule that a Group's frame +- `Timescale` 0 stays and means scale-less: keys and frame timestamps are + bare numbers, and a scale-less frame carries its group's key (decided + 2026-11-03, maintainer: ingested legacy tracks with bare keys still cross + lite-07; see [the model](/quest/m1/time-addressing/model.md)). FRAME and + datagrams always carry a timestamp. The untimed cases of + TIMESTAMP_MISMATCH, Time anchors, and arrival-age-only expiry become the + scale-less cases. The rule that a Group's frame 0 not start before a lower-sequence Group's becomes the strictly increasing start. diff --git a/quest/m1/time-addressing/model.md b/quest/m1/time-addressing/model.md index 184a1ae9d9b..ad7bd54d36f 100644 --- a/quest/m1/time-addressing/model.md +++ b/quest/m1/time-addressing/model.md @@ -15,9 +15,11 @@ Decided 2026-10-25 (maintainer). Why: group numbers need timelines just to map to and from time, and reusing a group number poisons caches, so resuming a producer would need per-track memory of its last number. -- **A group's address is its first frame's PTS**, known when the group opens. - Frames inside a group stay in decode order and their PTS may go backwards - (B-frames). +- **A group's address is its key.** A group our producers write is keyed by + its first frame's PTS, known when the group opens. A group ingested from a + legacy wire is keyed by the wire's group ID, verbatim (decided 2026-11-03, + below). Frames inside a group stay in decode order and their PTS may go + backwards (B-frames). - **Starts strictly increase.** An equal start is refused loudly like a rewind; `Neighbors::admit` (`model/track.rs`, about line 502) allows ties today. - **Three ways a group ends**: `finish(end)` with an exclusive end timestamp @@ -27,9 +29,9 @@ a producer would need per-track memory of its last number. - **Reach** (decided 2026-10-26, maintainer): a finished group's own end bounds it, else its successor's start, so the last group of a paused or finished track ages out without a successor (`TrackState::reach`). -- **Every track is timed.** Untimed goes away: the producer stamps an untimed - write with wall-clock time in microseconds, which stays under 2^53 (the - published JS decode limit, below) until the year 2255. +- **Every write takes a timestamp, and nothing is ever wall-clock stamped** + (decided 2026-11-03, below). A track without a timescale is scale-less: its + keys and frame timestamps are bare numbers in no known unit. - **Gaps.** The model keeps each group's previous start, so a cache or reader sees a hole without a round trip. The first group of an epoch has none, and the gap before it is unknown. Restarts keep minting a new epoch with fresh @@ -39,12 +41,10 @@ a producer would need per-track memory of its last number. Published `@moq/net` decodes a group ID with `u53` and refuses one past 2^53 (`js/net/src/lite/group.ts`), so a track whose starts pass that, such as nanosecond epoch time, fails those readers. -- **Legacy ingest.** A timed source keeps its timestamps. An untimed source - (moq-transport without TIMESCALE, lite-01..04) uses the group ID as the - timestamp on a synthetic scale, so relays agree and legacy egress - round-trips the exact ID; max age means nothing on those tracks. A timed - moq-transport group that starts at or before its predecessor ends the - track with an error. +- **Legacy ingest.** Every legacy wire's group ID is taken verbatim as the + key, timed or not, so relays agree and legacy egress round-trips the exact + ID (decided 2026-11-03, below). A group whose ID is at or before its + predecessor's ends the track with an error. - **Ranges.** `Absolute(group)` ends and sequence-range drops become times. `Relative(n)` stays but counts groups instead of subtracting from an ID. - **Name reuse** landed in @@ -81,6 +81,52 @@ it: SUBSCRIBE_START and its largest, SUBSCRIBE_END, SUBSCRIBE_DROP, FETCH, and every IETF Location, Filter, FETCH, Largest, and NEW_GROUP_REQUEST. +### Timestamps are required and legacy IDs pass through + +Decided 2026-11-03 (maintainer, the `/quest-import` grill of +[moq-dev/moq#7573](https://github.com/moq-dev/moq/issues/7573)). Why: a +wall-clock or `now()` stamp on a write lands in a different epoch than the +broadcast's media. That is what gives #7573's C publishers an 8 s catalog +`delay`, and the convenience it buys has no use case. This replaces the +2026-10-27 audit's "producer arguments keep an optional timestamp", which +recorded only sequencing. + +- **Required timestamps.** Every producer write in moq-net and every binding + takes a timestamp. There is no `None`, and nothing is wall-clock stamped. + Reads return a timestamp too. +- **Scale-less tracks.** A track with no timescale (moq-transport without + TIMESCALE, lite-01..04, or an app with no clock) is scale-less. Its frame + timestamp is its group's key, and the write API on a scale-less group takes + no per-frame time. Max-age on it counts bare units, so `max-age=5` keeps + groups within 5 IDs of the newest; the application knows how sparse its + IDs are. +- **One unit per track.** A track's timescale is the unit of both its frame + timestamps and its group keys. Legacy keys are assumed to be in that unit + and nothing checks it, so time ranges compare keys numerically. A released + publisher numbering groups 0, 1, 2 with ms frames gets ranges in ID units, + which is accepted. Max-age keeps comparing frame timestamps, as today. +- **Legacy IDs pass through.** Ingest keys a group by the wire's group ID on + every legacy wire, and egress writes the key back, so moq-transport group 6 + leaves as group 6 and reaches moq-lite as key 6. No group carries a sequence + beside its key. +- **Broadcasts are all timed or all scale-less.** moq-mux's catalog producer + stamps the catalog from the broadcast clock and refuses to list a track off + that clock's timescale, or a scale-less track beside timed ones. moq-net + tracks stay independent. +- **Open, for whoever starts this, with the maintainer:** how lite-05/06, + which carry both a sequence and frame timestamps, map on the way out. + +Decision prompts: + +- Ingest key: ✅ ID verbatim; first PTS, renumbered; per wire. +- A sequence beside the key: ✅ no, one key; yes, both. +- Timescale meaning: ✅ one unit for keys and frames, no legacy flag; flag + legacy keys. +- Enforcing timed broadcasts: ✅ the catalog producer; moq-net broadcast; + convention only. +- A scale-less frame's timestamp: ✅ its group key; any increasing number. +- Max-age on a scale-less track: ✅ bare units; doesn't apply. + ### Spike evidence The spike published groups at their start ticks (2 s GOPs at 1 kHz and at @@ -112,7 +158,8 @@ The PR's Impact section says how to migrate. Public API: breaking. `group::Info`, `create_group`, `append_group`, `Position`, `Range` ends, `fetch_group`, `Subscriber::floor`, datagrams, and -group consumers address by start timestamp, and untimed tracks go away. +group consumers address by start timestamp, every producer write takes a +required timestamp, and untimed tracks become scale-less. Wire: no format change; legacy and lite-07-wip group IDs become start ticks. ## Required