Skip to content

feat(net)!: routes carry an instance and a per-instance epoch - #7489

Merged
kixelated merged 58 commits into
mainfrom
quest/m1/api-audit/route/identity
Nov 2, 2026
Merged

kixelated merged 58 commits into
mainfrom
quest/m1/api-audit/route/identity

Conversation

@kixelated

@kixelated kixelated commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Implements Identity and epoch, now under the name instance. Every route in an origin carries an instance that names one continuous run of group numbers at its path, plus a crate-private per-instance epoch that 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 UUIDv7 Epoch is 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)

  1. ✅ (a) No CLI flag. The stable instance stays a route option in every library: Route::with_instance, announce({ instance, epoch }), MoqAnnounceRoute, and moq_announce_route. The CLI test (independent_importers_cannot_select_a_shared_instance) proves --instance is refused. The "Publisher runs" section of doc/bin/cli.md says why: no CLI importer continues its group sequences across a restart. Rejected: (b) --identity plus an epoch flag.
  2. ✅ Rename "identity" to "instance" throughout this PR before it lands, so there is one breaking change instead of two. "Identity" reads as who publishes, which clashes with auth subjects (moq-dev/moq#6992). The docs already said "publisher instance" and moq_mux::follow::Instances. Rejected: keep identity. 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:

    • the most specific prefix wins;
    • within one instance, only its highest epoch is eligible, even over a cheaper route;
    • then link preference, an identified chain, cost, a local broadcast, hop length, and a hash of the instance and the requested path. A route that entered for exactly the path skips the hash, so a restarted publisher wins at once.

    Supersession is now checked against one per-instance Epochs table in both Rust and JS, so ranking is linear. Rust and JS share the order and the hash vectors.

  • Read side:

    • request(path, instance) and follow(path, instance) pin an instance, and a pinned lookup follows that instance's own routes.
    • Moving to another instance ends the broadcast, and follow yields the next one.
    • broadcast::Info.instance and JS Broadcast.Consumer.instance replace epoch. 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), JS announce({ instance, epoch }), MoqAnnounceRoute.instance/epoch, or moq_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) and Epoch (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-restart Release, 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:

    • hang catalog references carry an optional instance, and moq-mux checks a pin against the resolved instance (best effort against a minted one);
    • archive keys are <path>/@<instance>, and moq-hls serves each stream under @<instance>;
    • moq-e2ee keeps its own UUIDv7 epoch and announces it as the route instance;
    • moq-stats keys nodes by instance;
    • moq-sign and @moq/sign bind the route instance and refuse a minted one (failure code instance).
  • Bindings: moq-ffi, moq-c, Python, Swift, Kotlin, Go, Dart, C++ and OBS name streams by instance. moq_mint_epoch and the Epoch wrappers are gone.

  • Docs, drafts, MIGRATE notes, quests:

    • doc/concept/identity.md keeps its title. Its first section now reads "Identity is a path and an instance" and carries the cloud-instance analogy.
    • In doc/concept/moq-lite.md, the section is "Publisher instances" (#publisher-instances), and every link to it is updated.
    • The lite draft's field names and text changed. The sign draft binds bytes(instance), and its vectors keep their bytes.
    • Every unreleased MIGRATE note that still described the epoch API now names the instance API. No release ever shipped a publisher epoch.
    • The quests that name the field were renamed too, including instance-encoding.md.

The rename

refactor(net)!: rename the route identity to instance is one mechanical commit, separate from logic, so it reviews as a rename. It renames only lines this PR adds: identity, Identity, and IDENTITY become instance, Instance, and INSTANCE, 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:

  • the wire Instance interface is Served;
  • the resume models' per-publisher state is Publisher;
  • moq-gst and moq-srt use id beside an existing instance.

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:

  • the new hls test and the new TrackServe/Track test literals;
  • the moq-transport route-change cases;
  • main's lite-07 withdrawal-while-opening test, which now runs on lite-06, since only a minted instance ends with its route.

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):

  • the binding docs take main's timestamps and this branch's announce;
  • feat(c)!: close epoch and audio query gaps #7246's malformed-epoch C cases are dropped, since any non-empty text is an instance, and the instance parse keeps its MOQ_ERROR_INVALID_CONFIG mapping in one helper;
  • main's new relay-hops C test and resume-history bench are ported onto instances.

The latest merges bring in origin/main through 998721a66a. That covers moq-dev/moq#7371 (the publish Publication refactor), moq-dev/moq#7457 (a one-shot session's draining), moq-dev/moq#7484 (required announcement captures), and moq-dev/moq#7604 (the archives concept page):

  • the conflicts in js/publish/src/broadcast.ts, js/net/src/wire.ts, doc/lib/js/net.md, doc/bin/hls.md, py/moq-rs/README.md, and rs/moq-ffi/src/origin.rs take main's code with this branch's instance naming;
  • main's new doc/concept/archives.md names the instance instead of the epoch;
  • main's archive link fix in the old identity-encoding.md quest moves to the renamed instance-encoding.md;
  • rs/moq-net/src/lite/publisher.rs takes moq-dev/moq#7500's take_track beside this branch's Request::instance;
  • the route epic's Related list drops the grant-subject quest that moq-dev/moq#6992 deleted;
  • main added 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 names route_dies_across_instances and asks lite-07-wip for two routes with different instances.

Fixes from the review of c1953d0675

  • A follower re-resolves only once its instance stops serving (Codex, medium). Following took 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 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 arms origin::Moved, a watch on the routes covering the path. It fires once the instance no longer serves the path under lookup'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 test a_follower_dropping_its_broadcast_waits_for_another_instance fails without the fix.
  • The C and FFI tests that asserted re-delivery on free now assert the opposite: follow_waits_for_another_instance_after_a_freed_takeover (moq-c) and follow_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 3e27d7b003

  • A pinned JS request follows its own instance's routes (Codex P2). With two instances live at a path, request(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.
  • An ended advertisement retires its minted instance (Codex P2). Announce streams overlapping at a path shared a minted instance by holder count, so a replacement delivered as END1, START1, END2, START2 never dropped the count to zero and inherited the old instance. The peer ending an advertisement on any stream now retires it, in Rust and JS. A stream closing only lets go of it.
  • A received instance is logged escaped (pullfrog thread). An instance is any non-empty text now, so moqsrc, moq play, the catalog producer and the TS follower log it with Debug.
  • A named instance should not start with ~ (pullfrog). Documented on Route::with_instance, JS Route.instance, and doc/concept/identity.md. It is not refused, since a relay forwarding a route keeps the instance it received.

Impact

Public API: breaking (feat!).

  • Rust moq-net:
    • Epoch and InvalidEpoch are gone. Use Instance (non-empty text, Instance::random(), is_minted()).
    • Route::with_epoch(e) becomes Route::with_instance(instance, epoch), and Route.epoch becomes Route.instance: Option<Instance>.
    • broadcast::Info.epoch becomes instance.
    • Consumer::request and follow take impl Into<Option<Instance>>.
    • Request::prepare(epoch) becomes prepare().
    • lite::Version::has_epoch becomes has_instance.
    • Most publishers just delete their with_epoch(Epoch::mint()), since every broadcast mints its own.
  • Other Rust crates:
    • hang: catalog references' epoch becomes instance.
    • moq-mux: Error::EpochMismatch becomes InstanceMismatch.
    • moq-sign: epoch() becomes instance(), and Error::Epoch becomes Error::Instance.
    • moq-e2ee: moq_e2ee::Epoch replaces moq_net::Epoch. Announce with with_instance(generation.epoch().instance(), 0).
  • JS:
    • @moq/net: Epoch becomes Instance, and announce({ epoch }) becomes announce({ instance, epoch }), or nothing. Broadcast.Consumer.epoch becomes .instance. Requests pin with request(path, { instance }) and follow(path, { instance }).
    • @moq/sign: epoch becomes instance, and failure code epoch becomes instance.
    • @moq/e2ee owns its Epoch.
  • Bindings: moq_mint_epoch is removed. Routes take an optional instance and an epoch, and requests and follows pin an instance string.
  • Catalog: no compatibility key is needed. Neither epoch nor identity ever shipped on release: git grep of origin/release finds no reference epoch in rs/hang or js/hang catalogs.
  • moq.pro: recording resume compares 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) and Epoch (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.json already marks every cross-version moq-lite-07-wip cell 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

  • Instances are percent-encoded in paths (already planned): until it lands, a stable instance containing /, ?, % or # breaks HLS/DASH URLs and archive keys.
  • A job's exact route continues its claim's readers (already planned): replaces the cross-instance takeover kept here with an inherited instance.
  • Suggested: de-flake 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 main bf4e726f66, against 2 of 570 on this branch.
  • Suggested: moq_stats::GroupFrame::epoch reuses moq_net::Instance for its UUIDv7 text. A type of its own, as moq-e2ee has, would say what it is.
  • Suggested: rs/moq-net/tests/integration/routing_contracts calls its stream label epoch. Renaming it collides with that model's router identity.

Checks

On 8b3fc600e6, which merges origin/main 998721a66a. Main has since added only a039ca1be5, a moq-uring perf change that merges cleanly.

  • just check origin/main passed: Rust, JS, Python, C, C++, OBS, Kotlin, Go, Dart, drafts, docs and quests.
  • quest check --base origin/main passed.

Skipped locally: Swift (no toolchain installed) and the flake module (this host has no user namespaces). just test interop --all last ran on 63ce3c9a97. Nothing since then touches the wire.

Review

Codex gpt-6.1-sol, medium effort (quest review), against origin/main. Session 01a19d55. Round 4 reviewed head 8b3fc600e6 and had no findings.

  • Round 1 (head c1953d0675): front retirement treated as a restart (rs/moq-net/src/model/follow.rs:255, medium). Fixed in 06677bdf69; see "Fixes from the review of c1953d0675".
  • Round 2 (head 06677bdf69): no findings.
  • Round 3 (head 88b28705c1, after merging origin/main 24b04633a5 and fixing the moq-ffi twin test): no findings.
  • Round 4 (head 8b3fc600e6, after merging origin/main 998721a66a by hand): no findings. This is the fourth-round design checkpoint. The approach stands: the only change since round 1 is the narrower closure trigger in Following, 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: 01a19ad3 reviewed 5b07a6ab40 with no findings, after refuting percent-encoding again (planned as quest/m1/api-audit/route/instance-encoding.md). 01a19983 reviewed e54275cee1 with no findings, after fixing quadratic supersession. 01a19603 and 01a19626, through cefa9d924c, 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)

kixelated and others added 30 commits October 29, 2026 07:01
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
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>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ 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.pin plus pick/retain/closeUnheld/unservable let 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, JS Route.instance, and doc/concept/identity.md now 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 into parse_epoch/parse_instance, both mapping malformed input to MOQ_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 in origin-request.test.ts, and the main-merge porting ({value, scale} timestamps, announce_route_hops_cross_a_relay_session).

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

kixelated and others added 6 commits November 2, 2026 09:27
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 kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ 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): Following no longer treats any closure of its handed-out broadcast as a takeover. It now arms a new origin::Moved watch, which fires only when OriginState::still_serves says the handed-out instance stopped serving (another instance wins, or a pinned follower has no route of its own left). A new absorbed flag 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 Rust a_follower_dropping_its_broadcast_waits_for_another_instance.
  • Merged origin/main twice (84b6e3015, 8b3fc600e6), resolving the doc/bin/hls.md, py/moq-rs/README.md, and rs/moq-ffi/src/origin.rs (required captures: Vec<String>) conflicts, and combining main's take_track allocation work with this branch's instance pin in rs/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.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ 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_track with this branch's Request::instance in rs/moq-net/src/lite/publisher.rs. RequestServe still 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 four Request impls and their start signatures match the merged trait.
  • Merged main's Path::join stack-buffer change and the new announce_allocs one-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.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@kixelated
kixelated added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit e3e1cc8 Nov 2, 2026
9 of 10 checks passed
@kixelated
kixelated deleted the quest/m1/api-audit/route/identity branch November 2, 2026 20:01
kixelated added a commit that referenced this pull request Nov 3, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant