Repository navigation
feat(net)!: routes carry an instance and a per-instance epoch - #7489
Conversation
Quest-Claim: a5c32d05d9bb8b26
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Drop the EPOCH fields from main's new request-stream code, and update main's dynamic-epoch restart test and identity guide for epochless moq-transport.
Main's joining-FETCH test and the moq-transport-18 e2ee transport case still expected an epoch over moq-transport, and the priority-update restart reads lite-07 alone as in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's moq-cli replacement and route-change suites asserted an in-place restart or a cross-route resume over moq-transport, which carried the epoch until this cut. Epochless moq-transport route loss stays covered by the pinned cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep draft 20 and announce takeover from main without their EPOCH parameter, and drop main's MoQ Epoch extension tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…79e0eb2452b # Conflicts: # js/net/src/ietf/subscriber.test.ts # rs/moq-net/src/ietf/publisher/tracks.rs
Quest-Claim: 9952c428ab2a4d64
Every route in an origin now carries an `Identity`, non-empty text naming one continuous stream at its path, plus a crate-private `u64` epoch within it. The UUIDv7 `Epoch` is retired. - Ranking: the most specific prefix, then only an identity's highest epoch is eligible, then link preference, an identified chain, cost, a local broadcast, hop length, and a hash of the identity and path (a route that entered for exactly the path skips the hashes, so the newest wins), then within one identity a hash of the hop chain. - `request(path, identity)` and `follow(path, identity)` pin an identity; a pinned lookup follows that identity's own routes. Moving to another identity ends the broadcast and `follow` yields the next one. - A broadcast mints a random identity and keeps it across re-announcing; `Route::with_identity(identity, epoch)` names a stable one. - lite-07-wip: ANNOUNCE_START and ANNOUNCE_RESTART carry identity and epoch; TRACK, TRACKS, SUBSCRIBE and FETCH carry an optional identity pin. - Sessions mint a `~`-prefixed identity for each announcement a wire without identities delivers (older lite, moq-transport), so the origin holds no route without one. Fronts never re-splice a track through a minted identity. - Deleted the epochless machinery: route generations and renewals, `restarted_since`, the held-restart release, and Selection retirement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dentity A request through a claim pins the claim's identity on lite-07, so the job's exact route that takes the claim over must answer it: the lookup, the front's staleness check and the handed-out broadcast all accept a winner continuing the pinned identity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- hang: catalog references carry an optional `identity` in place of the UUIDv7 `epoch`, refused when empty; health observers name the identity. - moq-mux: a reference pin is checked against the resolved identity, best effort against one a session minted. - moq-cli: archive keys are `<path>/@<identity>`; `play` resolves renditions by identity. moq-hls serves each stream's URLs under `@<identity>`. - moq-e2ee keeps its own UUIDv7 epoch and announces it as the route identity. - moq-stats keys nodes by route identity and mints its own group-frame epoch. - Ingests (RTMP, SRT, WHIP, RTSP, GStreamer) publish under the identity each broadcast mints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- moq-ffi: `MoqAnnounceRoute { cost, identity, epoch }`, `MoqRoute.identity`,
`follow`/`request(path, identity)`, `prepare()` without an epoch, and
`MoqError::InvalidIdentity`. `moq_mint_epoch` is removed.
- moq-c: `moq_announce_route` carries `identity`, `identity_len` and `epoch`;
`moq_route.identity`; `moq_broadcast_request_prepare(request, broadcast)`.
`moq_mint_epoch` and `MOQ_EPOCH_LEN` are removed.
- Python, Kotlin, Swift, Dart, Go, C++ and the OBS plugin follow, dropping
their mint-epoch helpers. The CARLA sim binds a driver lease to the route
identity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors rs/moq-net: `Identity` replaces `Epoch`, `Broadcast.Consumer.identity`
replaces `.epoch`, `request`/`follow` take `{ identity }`, `request.prepare()`
takes no epoch, and routes rank by the same order and hashes. The lite-07-wip
codec carries identity and epoch; older lite and moq-transport sessions mint a
`~`-prefixed identity per announcement, shared across a session's announce
streams. A handle re-sources its tracks only under a carried identity.
@moq/hang's catalog references carry `identity`; @moq/watch refuses a rendition
whose broadcast proves another identity, best effort against a minted one;
@moq/e2ee keeps the UUIDv7 epoch as its own type. The interop clients report
the active route's identity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrites `doc/concept/identity.md` around stream identities, with one route order (most specific prefix, highest epoch within an identity, then rank among identities), minting over older wires, and pools. Updates the moq-lite concept page, redundancy (a redundant pair is two identities), CLI, relay, stats, binding and library pages, the moq-lite draft's announce and request fields, the hang draft's `identity` reference field, and the e2ee draft. Finishes the identity quest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local broadcast now takes part in supersession: a higher epoch of its identity announced by a session or claim wins over it, however cheap the local broadcast is, and the reverse. Supersession is decided after a reader's scope filtering rather than when the candidate table is built, so a higher epoch a reader cannot see never hides a route it can, as in rs/moq-net. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local broadcast is ineligible once any route at its path the request could take carries its identity at a higher epoch, not only the best one. A scope's projection keeps each merged covering prefix on its candidates, so supersession never compares routes of different prefixes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`localWins()` takes the same skip its caller gave `bestEntry()`, so a publisher that never forwards received routes is not disqualified by a higher epoch only a session delivered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dency Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Variables, fields, helpers, tests and comments that kept saying epoch after the port mean the route identity; rename them, and fix the docs that still named the removed epoch API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ute/identity # Conflicts: # doc/concept/hang.md # drafts/draft-lcurley-moq-hang.md # js/hang/api/catalog.api.md # js/hang/src/catalog/audio.ts # js/hang/src/catalog/root.test.ts # js/hang/src/catalog/video.ts # rs/hang/src/catalog/root.rs # rs/moq-cli/src/play/media.rs # rs/moq-cli/src/publish.rs # rs/moq-cli/tests/integration/export.rs # rs/moq-ffi/src/test.rs # rs/moq-mux/src/container/ts/export.rs # rs/moq-mux/src/container/ts/export_test.rs # rs/moq-net-tests/tests/integration/restart.rs # rs/moq-net/benches/origin.rs # rs/moq-net/src/ietf/peer.rs # rs/moq-net/src/ietf/publisher.rs # rs/moq-net/src/ietf/subscriber.rs # rs/moq-net/src/lite/publisher.rs # rs/moq-net/src/lite/subscriber.rs # rs/moq-net/src/model/follow.rs # rs/moq-net/src/model/origin.rs # rs/moq-net/src/model/resume_model.rs # rs/moq-net/tests/integration/catalog_acquisition.rs # rs/moq-net/tests/integration/catalog_churn.rs # rs/moq-net/tests/integration/extensions.rs # rs/moq-net/tests/integration/mesh_withdraw.rs # rs/moq-net/tests/integration/multi_cdn.rs # rs/moq-net/tests/integration/replacement.rs # rs/moq-net/tests/integration/route_change.rs # rs/moq-rtmp/src/dial.rs # rs/moq-rtmp/src/server.rs # rs/moq-stats/src/aggregate.rs
…rmed epoch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review (3e27d7b0), the two prior findings are fixed, alongside the ~ and log-escaping follow-ups, tests, and a main merge.
- JS pinned-request resolution (
js/net/src/origin.ts):RequestSlot.pinpluspick/retain/closeUnheld/unservablelet a request pinned to a non-winning instance resolve through that instance's own routes on a detached slot rather than select the path's winner and discard it; the shared session answer is now released only once neither the cache nor a detached slot holds it. - Session-minted instance retirement (
rs/moq-net/src/lite/subscriber.rs,js/net/src/lite/subscriber.ts):Minted::release(path, instance, ended)retires the instance when the peer ends its advertisement on any announce stream, while a plain stream close only drops one holder, so an interleaved replacement (END1 START1 END2 START2) no longer inherits the old instance and splices the two runs. - Received instances logged escaped (moqsrc,
moq play, the catalog producer, the TS follower): an instance is any non-empty text, so it is now logged with?. ~marker documented, not refused:Route::with_instance, JSRoute.instance, anddoc/concept/identity.mdnow say a named instance should not start with~(it disables continuity and sign binding) while a relay still forwards the instance it received.- moq-c (
rs/moq-c/src/api.rs): epoch parsing split intoparse_epoch/parse_instance, both mapping malformed input toMOQ_ERROR_INVALID_CONFIG, with refusal tests and a route crossing a relay session. - Tests:
an interleaved replacement mints a fresh instance(Rust and JS), pinned request/follow coverage inorigin-request.test.ts, and the main-merge porting ({value, scale}timestamps,announce_route_hops_cross_a_relay_session).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Conflicts resolved by hand: - js/publish/src/broadcast.ts: main's Publication refactor, its comment naming the instance. - js/net/src/wire.ts: main dropped the Drain import, this branch the Epoch import. - doc/lib/js/net.md: this branch's instance wording plus main's one-shot GOAWAY sentences. - Cargo.lock: main's lockfile, regenerated for this branch's uuid moves.
Main added quest/m1/epochless-leave-restart.md, written for epochless routes, which this branch removes: older wires mint an instance per announcement, and the test it names is now route_dies_across_instances.
Following treated any closure of the broadcast it handed out as another instance taking the path over. A front also closes once its last holder lets it go, so a caller that dropped the handle, as moq_mux::follow::Instances does with one of the wrong instance, made the follower ask the upstream again and hand the same instance out, over and over, with nothing changed. The closure now only arms a watch on the routes covering the path, which fires once the instance no longer serves it as a lookup would resolve it: another instance winning, or for a pinned follower, no route of its own left. A restart the follower absorbed into the still-open handle stays owed when that handle closes. moq-c's follow_delivers_a_freed_takeover_again asserted the old re-delivery: the takeover's restart reaches the follower before its resolution, so no restart is owed. It now asserts that a freed takeover waits for the next instance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved by hand: - doc/bin/hls.md: main's --connect-version flag, this branch's @<instance> key. - py/moq-rs/README.md: this branch's instance wording, main's required captures. - rs/moq-ffi/src/origin.rs: main's required captures, this branch's route doc. - quest identity-encoding.md: renamed here to instance-encoding.md; main's archive link fix is carried over. Also names the instance in main's new doc/concept/archives.md.
moq-ffi's follow_hands_out_a_dropped_takeover_again asserted the re-delivery on drop that 06677bd removed, like moq-c's twin: the takeover's restart reaches the follower before its resolution, so no restart is owed. It now asserts the dropped takeover is not handed out again and the next instance is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved by hand: - rs/moq-net/src/lite/publisher.rs: main's take_track, this branch's instance pin in place of the epoch. - quest/m1/api-audit/route/README.md: this branch's Related list, without the grant-subject quest main deleted. - quest/m1/api-audit/route/identity.md: stays deleted, since this branch finishes it.
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 88b28705c19d67330537254ab8507d0408554964.
No new actionable bugs found. The follower now distinguishes a caller releasing its delivered handle from that instance ceasing to serve the path, preventing repeated resolution of an unchanged instance. The route watch reuses existing lookup ranking and preserves pinned-instance/takeover semantics; the absorbed-restart flag retains an outstanding change until it can be delivered. Rust, C, and FFI regression cases reflect the revised contract.
Both P2 findings reported fixed in the previous review remain addressed. Direction is sound: watching route lifetime fixes the retry loop at its source without adding retries or a second route-selection policy.
Verification: static incremental review of the follower, route-watch/lookup code, regression tests, and relevant merge adaptations, separating incoming main changes. No tests, builds, interop runs, or performance measurements executed; this is not an exhaustive re-review of the full cross-language migration.
(Written by OpenAI)
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run covered the commits since the prior pullfrog review (5b07a6ab) through the current branch tip 8b3fc600e6. Two independent traces (closure trigger, and still_serves/lookup parity) found no reachable defect.
- Narrowed the follow re-resolution trigger (
06677bdf6):Followingno longer treats any closure of its handed-out broadcast as a takeover. It now arms a neworigin::Movedwatch, which fires only whenOriginState::still_servessays the handed-out instance stopped serving (another instance wins, or a pinned follower has no route of its own left). A newabsorbedflag keeps a restart that was deduplicated against the still-open handle owed, handed out once when that handle later closes. - Updated the C and FFI tests to assert no re-delivery on a freed/dropped handle while the same instance serves (
follow_waits_for_another_instance_after_a_freed_takeover,..._after_a_dropped_takeover), and added Rusta_follower_dropping_its_broadcast_waits_for_another_instance. - Merged
origin/maintwice (84b6e3015,8b3fc600e6), resolving thedoc/bin/hls.md,py/moq-rs/README.md, andrs/moq-ffi/src/origin.rs(requiredcaptures: Vec<String>) conflicts, and combining main'stake_trackallocation work with this branch's instance pin inrs/moq-net/src/lite/publisher.rs. The resolved files read coherently.
One optional note: the absorbed == true plus handle-drop combination is confirmed correct by analysis (at most one re-hand-out per absorbed change, then it clears) but is not directly tested; the suite pins absorbed + held and non-absorbed + drop. A case that drops a handle while absorbed is set would close that gap.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run covered the net delta since the prior pullfrog review (88b28705c1): the hand-resolved merge of origin/main at 998721a66 (8b3fc600e6).
- Combined main's
Request::take_trackwith this branch'sRequest::instanceinrs/moq-net/src/lite/publisher.rs.RequestServestill reads the instance pin from the message for the origin lookup, then moves the track name into the log context when the request starts (and on a graceful early close), so neither the instance pin nor main's one-copy track handling is lost. The fourRequestimpls and theirstartsignatures match the merged trait. - Merged main's
Path::joinstack-buffer change and the newannounce_allocsone-copy test unchanged; the instance work touches neither path. - Dropped the deleted grant-subject quest from the route epic's Related list and finished the identity-to-instance wording in the quest docs.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Carry `Following` onto main's `Origin.Reader` and its instance pin: the follower docs now say "instance" where #7489 replaced the epoch pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Implements Identity and epoch, now under the name instance. Every route in an origin carries an
instancethat names one continuous run of group numbers at its path, plus a crate-private per-instanceepochthat fences out older runs. Think of a cloud instance ID that survives a reboot, with the epoch counting the boots. A broadcast's identity is its path plus its instance. The UUIDv7Epochis retired, and the origin holds no route without an instance.Earlier on 2026-11-01 the maintainer said "we're replacing EPOCH with IDENTITY soon". This PR is that work, now named "instance".
Decisions (maintainer, 2026-11-01)
Route::with_instance,announce({ instance, epoch }),MoqAnnounceRoute, andmoq_announce_route. The CLI test (independent_importers_cannot_select_a_shared_instance) proves--instanceis refused. The "Publisher runs" section ofdoc/bin/cli.mdsays why: no CLI importer continues its group sequences across a restart. Rejected: (b)--identityplus an epoch flag.moq_mux::follow::Instances. Rejected: keepidentity. The route epic (quest/m1/api-audit/route/README.md) records both decisions. The quest this PR completes is deleted.Summary
Ranking (
rs/moq-net/src/model/origin.rs,js/net/src/internal.ts), in this order:Supersession is now checked against one per-instance
Epochstable in both Rust and JS, so ranking is linear. Rust and JS share the order and the hash vectors.Read side:
request(path, instance)andfollow(path, instance)pin an instance, and a pinned lookup follows that instance's own routes.followyields the next one.broadcast::Info.instanceand JSBroadcast.Consumer.instancereplaceepoch. The epoch is never readable.Publish side: a broadcast mints a random instance when it is created and keeps it across withdraw, re-announce, and reconnect. A publisher names a stable one with
Route::with_instance(instance, epoch), JSannounce({ instance, epoch }),MoqAnnounceRoute.instance/epoch, ormoq_announce_route.instance/epoch, all documented with the promise and its cost.Request::prepare()takes no epoch.Wire, lite-07-wip: ANNOUNCE_START, ANNOUNCE_RESTART and ANNOUNCE_TAKEOVER carry
Instance (b)andEpoch (i)in place of the 16-byte UUID. TRACK, TRACKS, SUBSCRIBE and FETCH carry an optional instance pin. Released versions keep their framing.Minting off the wire: a session mints a
~-prefixed instance, epoch 0, for every announcement an older lite or moq-transport peer delivers. A job re-announced on one session is therefore never spliced to its predecessor (the bug in moq-dev/moq#5678). Fronts never re-splice a track through a minted instance.Deleted the epochless machinery: route generations and renewals,
restarted_since, the held-restartRelease, Selection retirement,Resolution, and the epochless refusal.ANNOUNCE_TAKEOVER keeps working until the announce takeover quest lands. It landed in moq-dev/moq#6994, and that quest removes it. A takeover now records its predecessor's instance. A lookup, a front's staleness check, and the handed-out broadcast all accept a winner that continues the instance a request pinned.
Catalogs, archives and tools:
instance, and moq-mux checks a pin against the resolved instance (best effort against a minted one);<path>/@<instance>, and moq-hls serves each stream under@<instance>;@moq/signbind the route instance and refuse a minted one (failure codeinstance).Bindings: moq-ffi, moq-c, Python, Swift, Kotlin, Go, Dart, C++ and OBS name streams by instance.
moq_mint_epochand theEpochwrappers are gone.Docs, drafts, MIGRATE notes, quests:
doc/concept/identity.mdkeeps its title. Its first section now reads "Identity is a path and an instance" and carries the cloud-instance analogy.doc/concept/moq-lite.md, the section is "Publisher instances" (#publisher-instances), and every link to it is updated.bytes(instance), and its vectors keep their bytes.instance-encoding.md.The rename
refactor(net)!: rename the route identity to instanceis one mechanical commit, separate from logic, so it reviews as a rename. It renames only lines this PR adds:identity,Identity, andIDENTITYbecomeinstance,Instance, andINSTANCE, plurals and identifiers included. So no "identity" that main already had changes: TLS, auth, codesign, e2ee failure codes, a track's or a frame's identity, JS object identity, and the first-hop identity all stay. Its message lists the few places where a name collided and was resolved by hand:Instanceinterface isServed;Publisher;idbeside an existinginstance.The commits that follow it are the merge fixes, the docs and quests, and the review's perf fix.
Merging main
This branch merges origin/main, conflicts resolved by hand. Main gained moq-dev/moq#7239, the moq-transport epoch removal, which this branch had merged earlier. It also gained moq-dev/moq#7328, which sends SUBSCRIBE alongside TRACK, and moq-dev/moq#7399, which ends a replaced HLS recording. Ported onto instances:
TrackServe/Tracktest literals;Main's "listing of an epochless handle" test is dropped. TRACKS exists only on lite-07, where every route names its instance and the pin rides the request, so the case has no wire left to run on.
A later merge brought in moq-dev/moq#6946 (exact
{value, scale}timestamps in every binding), moq-dev/moq#7246 (moq-c epoch validation), and moq-dev/moq#7558 (moqsrc's session refactor):MOQ_ERROR_INVALID_CONFIGmapping in one helper;resume-historybench are ported onto instances.The latest merges bring in origin/main through
998721a66a. That covers moq-dev/moq#7371 (the publishPublicationrefactor), moq-dev/moq#7457 (a one-shot session'sdraining), moq-dev/moq#7484 (required announcement captures), and moq-dev/moq#7604 (the archives concept page):js/publish/src/broadcast.ts,js/net/src/wire.ts,doc/lib/js/net.md,doc/bin/hls.md,py/moq-rs/README.md, andrs/moq-ffi/src/origin.rstake main's code with this branch's instance naming;doc/concept/archives.mdnames the instance instead of the epoch;identity-encoding.mdquest moves to the renamedinstance-encoding.md;rs/moq-net/src/lite/publisher.rstakes moq-dev/moq#7500'stake_trackbeside this branch'sRequest::instance;quest/m1/epochless-leave-restart.md, which is about epochless routes, and this branch has none. It is now A follower is told when the serving instance's route leaves, which namesroute_dies_across_instancesand asks lite-07-wip for two routes with different instances.Fixes from the review of
c1953d0675Followingtook any closure of the broadcast it handed out as another instance taking the path over. A front also closes once its last holder lets it go. So a caller that dropped the handle, asmoq_mux::follow::Instancesdoes with one of the wrong instance, made the follower ask upstream again and hand out the same instance, over and over, with nothing changed. A probe confirmed it: the host got a fresh request every cycle. Now the closure only armsorigin::Moved, a watch on the routes covering the path. It fires once the instance no longer serves the path underlookup's own rule: another instance wins, or, for a pinned follower, no route of its own is left. The one exception is a restart the follower deduplicated against the still-open handle; it stays owed when that handle closes. JS already re-resolved only on a changed winning instance (origin.ts,follow), so the two now agree. The new testa_follower_dropping_its_broadcast_waits_for_another_instancefails without the fix.follow_waits_for_another_instance_after_a_freed_takeover(moq-c) andfollow_waits_for_another_instance_after_a_dropped_takeover(moq-ffi). Their premise was a takeover restart held past its resolution. A trace shows that restart reaches the follower before the resolution on this branch, so nothing was owed, and the tests passed only through the churn above.Fixes from the review of
3e27d7b003request(path, { instance: A })used to share the path's slot, materialize the winner B, then report Unroutable. A pin naming another instance than the winner now resolves on a detached slot of its own, through that instance's best route, as Rust's lookup does. A peer's pin (local and demand) likewise competes only with its own instance's routes. A session answers one front per path, so the pinned slot and the path's requests can share one, which closes only once neither holds it.moq play, the catalog producer and the TS follower log it withDebug.~(pullfrog). Documented onRoute::with_instance, JSRoute.instance, anddoc/concept/identity.md. It is not refused, since a relay forwarding a route keeps the instance it received.Impact
Public API: breaking (
feat!).moq-net:EpochandInvalidEpochare gone. UseInstance(non-empty text,Instance::random(),is_minted()).Route::with_epoch(e)becomesRoute::with_instance(instance, epoch), andRoute.epochbecomesRoute.instance: Option<Instance>.broadcast::Info.epochbecomesinstance.Consumer::requestandfollowtakeimpl Into<Option<Instance>>.Request::prepare(epoch)becomesprepare().lite::Version::has_epochbecomeshas_instance.with_epoch(Epoch::mint()), since every broadcast mints its own.hang: catalog references'epochbecomesinstance.moq-mux:Error::EpochMismatchbecomesInstanceMismatch.moq-sign:epoch()becomesinstance(), andError::EpochbecomesError::Instance.moq-e2ee:moq_e2ee::Epochreplacesmoq_net::Epoch. Announce withwith_instance(generation.epoch().instance(), 0).@moq/net:EpochbecomesInstance, andannounce({ epoch })becomesannounce({ instance, epoch }), or nothing.Broadcast.Consumer.epochbecomes.instance. Requests pin withrequest(path, { instance })andfollow(path, { instance }).@moq/sign:epochbecomesinstance, and failure codeepochbecomesinstance.@moq/e2eeowns itsEpoch.moq_mint_epochis removed. Routes take an optionalinstanceand anepoch, and requests and follows pin an instance string.epochnoridentityever shipped onrelease:git grepoforigin/releasefinds no reference epoch inrs/hangorjs/hangcatalogs.routeEpoch(api/src/recording.ts), and recordings are keyed<path>/@<epoch>/.... Key them on(path, instance)instead, and record each run's start time on moq.pro's side, since an epoch only orders one instance's runs and is no clock. The sync-ingest mirror keeps forwarding the route it received, and its instance now crosses with it.Wire: lite-07-wip announce messages carry
Instance (b)andEpoch (i)in place of the UUIDv7. TRACK, TRACKS, SUBSCRIBE and FETCH carry an instance pin in place of the epoch. moq-lite 01-06 and moq-transport are unchanged.Choices made without asking
A minted instance is
~plus 128 random bits, not the session ID plus the announcement ID. Either is unique per announcement, which is all the plan relies on. Random needs no session state and is URL-safe, which instances in paths assumes.The lookup and the front accept a takeover's successor for a request pinned to its predecessor's instance. This keeps the first-subscribe continuity of moq-dev/moq#6994 on lite-07 until the announce-takeover quest replaces it with an inherited instance.
moq-sign binds the route instance directly, with no separate epoch type, since an instance is exactly one sequence space. moq-e2ee keeps its own UUIDv7 epoch because its keys derive from it.
No split: the type change crosses every crate and binding at once, and nothing compiles halfway, so the PR lands as one.
The compat lane needs no new entry.
test/interop/compat/planned-breaks.jsonalready marks every cross-versionmoq-lite-07-wipcell as a planned break while that draft is unpublished.follow(Rust, C, and every binding) no longer re-delivers a handle the caller freed while the same instance still serves the path. The next delivery comes when another instance takes over, or after an end. The old re-delivery was undocumented outside two tests, and it is what made a dropping caller churn upstream requests.Follow-ups
/,?,%or#breaks HLS/DASH URLs and archive keys.js/net/src/lite/connection.test.ts's close-drain tests ("a peer close drains the overtaken group/fin", "close delivers a finished track's final group"). They also fail on main: 3 of 570 runs on mainbf4e726f66, against 2 of 570 on this branch.moq_stats::GroupFrame::epochreusesmoq_net::Instancefor its UUIDv7 text. A type of its own, as moq-e2ee has, would say what it is.rs/moq-net/tests/integration/routing_contractscalls its stream labelepoch. Renaming it collides with that model's router identity.Checks
On
8b3fc600e6, which merges origin/main998721a66a. Main has since added onlya039ca1be5, a moq-uring perf change that merges cleanly.just check origin/mainpassed: Rust, JS, Python, C, C++, OBS, Kotlin, Go, Dart, drafts, docs and quests.quest check --base origin/mainpassed.Skipped locally: Swift (no toolchain installed) and the flake module (this host has no user namespaces).
just test interop --alllast ran on63ce3c9a97. Nothing since then touches the wire.Review
Codex
gpt-6.1-sol, medium effort (quest review), against origin/main. Session01a19d55. Round 4 reviewed head8b3fc600e6and had no findings.c1953d0675): front retirement treated as a restart (rs/moq-net/src/model/follow.rs:255, medium). Fixed in06677bdf69; see "Fixes from the review ofc1953d0675".06677bdf69): no findings.88b28705c1, after merging origin/main24b04633a5and fixing the moq-ffi twin test): no findings.8b3fc600e6, after merging origin/main998721a66aby hand): no findings. This is the fourth-round design checkpoint. The approach stands: the only change since round 1 is the narrower closure trigger inFollowing, which matches what JS already did. Recommendation: merge.This session started fresh. The previous one (
01a19ad3) was keyed to a local branch name this checkout could not use, since a stale worktree holds it. Earlier sessions:01a19ad3reviewed5b07a6ab40with no findings, after refuting percent-encoding again (planned asquest/m1/api-audit/route/instance-encoding.md).01a19983reviewede54275cee1with no findings, after fixing quadratic supersession.01a19603and01a19626, throughcefa9d924c, fixed a BOM strip in wire instances.Automated reviews on
5b07a6ab40(OpenAI, pullfrog) found nothing new, and every earlier thread has an answer.🤖 Generated with Claude Code
(Written by Claude Opus 5.5)