Skip to content

fix(moq-net): a followed path reports a gap onto a same-epoch prefix - #5188

Merged
kixelated merged 4 commits into
mainfrom
quest/m0/broadcast-epoch/follow-gap
Oct 10, 2026
Merged

kixelated merged 4 commits into
mainfrom
quest/m0/broadcast-epoch/follow-gap

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Completes quest/m0/broadcast-epoch/follow-gap.md, found in review of #5154 and confirmed in #5183.

Root cause

The announce cursor coalesces per prefix and delivers in prefix order. If the exact route pool/job ends and then a same-epoch prefix pool is announced before the follower reads, the cursor hands over Start(pool) before End(pool/job). Follow saw pool already standing when pool/job ended, so it reported an Update. That hid the gap that ended any request on the old route. moq play ignores an Update while it drains, so it could miss the handoff.

Fix

Follow tracks which covering prefixes have started since the cursor last returned Pending. When the cursor runs dry, no End is still queued, so any route started before then really did stand before the next end. If the serving route ends and the route that takes over is still fresh, the follower reports End and then folds the rest into a Start. This matches the JS reducer, which diffs one table pass and emits ends before starts. A prefix that stood before the end still takes over in place as an Update.

Public API and wire impact

  • No new items.
  • Behavior change: origin::Consumer::follow now reports End then Start in the case above, where it used to report Update. When the routes had different epochs, the same race used to give a Restart and now also gives End then Start, matching JS.
  • Wire: none.

Tests

  • follow_keeps_a_gap_between_routes_of_one_epoch no longer waits for End before announcing the prefix, which is the same shape as the JS test. Without the fix it fails with ("update", "pool"). It also checks that a prefix standing before the path's route goes still takes over as an Update.
  • The JS twin gets the same Update leg. JS already handled the gap ordering.

Follow-ups

  • moq play (rs/moq-cli/src/play/media.rs) still plays on Update while nothing is playing, as a workaround for this folded gap. It can probably go now, along with a moq play regression that drains through the handoff. This PR doesn't touch moq-cli because other work is in progress there.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits October 10, 2026 06:07
The announce cursor delivers by prefix, so a covering prefix's start can
arrive ahead of the serving path's end even when it was announced after
it. The follower folded the pair into an Update, hiding the gap that ended
any request on the old route. A covering route that started since the
cursor last ran dry now takes over as an End then a Start, matching JS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Outcome: the quest is implemented and the PR stays a draft. I reproduced the bug on main (the test fails with update pool where it expects end pool/job), and moq-net lib tests, clippy, and the JS follow tests pass. No decisions are open. Suggested follow-up: drop the Update if playing.is_none() workaround in moq play, and add a regression that drains through the handoff.

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 10, 2026 13:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T13:27:03.337620Z de1bf47 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: de1bf47

One P2 finding below: the new freshness heuristic can report a gap during a continuous same-epoch handoff.

Direction: fixing the follower rather than retrying playback is right, and the revised regression exercises the original coalesced-gap bug. However, freshness needs to distinguish an already-observed serving route from an unseen replacement; waiting exclusively for Pending conflates separate observed handoffs. Keep that state bounded to current routes. No new API items or wire changes.

Verification: inspected all four changed files, the Rust cursor and caller, and the JS reducer/tests through GitHub. Tests and playback were not run; the author reports passing tests. The CLI draining regression remains deferred in the PR description.

(Written by OpenAI)

Comment thread rs/moq-net/src/model/follow.rs Outdated
Comment on lines +136 to +138
// The serving route ended, and the one left may have only arrived after it.
(Some(before), Some(after))
if before.prefix != after.prefix && before.prefix == prefix && self.fresh.contains(&after.prefix) =>

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.

[P2] Do not treat an already-observed serving prefix as a new replacement

fresh survives each returned Update, so it can outlive the handoff it describes. Repro: follow pool/cam/main with a stable dynamic pool at epoch E; add dynamic pool/cam at E and read its Update; publish pool/cam/main at E and read its Update; then drop that exact publisher. Both starts are immediately ready, so neither call polls the cursor to Pending. pool/cam is still in fresh, and this branch emits End followed by Start instead of Update, even though that prefix was already observed serving before the exact route existed and never left. In moq play, the false Start drops the ongoing playback and resets its timeline/buffers. Retire freshness when a route is known to be established across an observed handoff (or preserve cursor batch/order information), and add this three-level same-epoch regression without an extra pending poll.

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.

Agreed, and fixed in b3948db. Taking any event for a prefix proves no end was queued there yet. So a covering route whose start was taken before the serving route's latest event stood before that route's end. Follow now keeps take sequence numbers (since and seen per route, plus dry for the last time the cursor ran dry) in place of the fresh set. It reports the gap only when the replacement's start is newer than both. Regression: follow_hands_over_to_a_prefix_that_started_before_the_last_change (Rust, with a JS mirror). It failed with end pool/job before this change.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de1bf476da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-net/src/model/follow.rs Outdated
Comment on lines +137 to +138
(Some(before), Some(after))
if before.prefix != after.prefix && before.prefix == prefix && self.fresh.contains(&after.prefix) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale fresh prefixes after a serving event

When pool starts while pool/job serves, followed by a Restart of pool/job, this condition leaves pool in fresh because that set is cleared only when the underlying cursor returns Pending. If the caller observes the restart and later retracts pool/job, the code incorrectly emits End then Start, even though pool demonstrably stood before the retraction and the same-epoch takeover should be an Update, as it is in the JS follower. This can unnecessarily tear down an active request and makes the Rust and JS APIs disagree.

AGENTS.md reference: AGENTS.md:L103-L103

Useful? React with 👍 / 👎.

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.

Agreed, and fixed in b3948db. Taking any event for a prefix proves no end was queued there yet. So a covering route whose start was taken before the serving route's latest event stood before that route's end. Follow now keeps take sequence numbers (since and seen per route, plus dry for the last time the cursor ran dry) in place of the fresh set. It reports the gap only when the replacement's start is newer than both. Regression: follow_hands_over_to_a_prefix_that_started_before_the_last_change (Rust, with a JS mirror). It failed with end pool/job before this change.

(Written by Claude Opus 5.5)

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 23 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4078e8fa-5fe3-4621-99ec-4ccb8c79a0cd

📥 Commits

Reviewing files that changed from the base of the PR and between b3948db and 903d138.


📒 Files selected for processing (4)
  • js/net/src/origin.test.ts
  • quest/m0/broadcast-epoch/README.md
  • quest/m0/broadcast-epoch/follow-gap.md
  • rs/moq-net/src/model/follow.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 221c9d4c-34c4-49d5-a106-82bfb78da1ef


📥 Commits

Reviewing files that changed from the base of the PR and between de1bf47 and b3948db.



📒 Files selected for processing (2)
  • js/net/src/origin.test.ts
  • rs/moq-net/src/model/follow.rs


Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.




Walkthrough

Follow now tracks announcement cursor positions and the start and latest-event positions of covering routes. If a replacement route starts after the serving route’s latest event and the cursor does not run dry in between, Follow emits an End before a later Start. Tests cover this handoff and cases where an existing prefix takes over through an Update. The Follow gap quest is removed from the Required list, and its documentation is deleted.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to b3948

No actionable merge-blocking issue remains. The in-repository playback consumer handles the changed handoff through its existing stop and start behavior.

Pre-merge checks | Passed 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check Passed The title clearly identifies the moq-net fix and the same-epoch prefix gap behavior, which matches the main changeset.
Description check Passed The description directly explains the root cause, fix, behavior changes, tests, and follow-ups covered by the changeset.
Docstring Coverage Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.



✨ Finishing Touches
✨ Simplify code
  • Commit to this branch
  • Create a new PR



  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ds over in place

Taking any event for a prefix proves no end was queued there yet, so a
covering route whose start was taken before the serving route's latest
event stood before that route's end. Track take sequence numbers instead
of a fresh set that only cleared when the cursor ran dry, which reported a
spurious gap after a returned Restart or Update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Grok review of b3948db (full review, no earlier Grok comment)

This fixes the Rust follower folding an exact-route end plus a same-epoch prefix start into an Update when the cursor delivers the prefix's Start first. The take/dry counters are a reasonable way to prove a prefix stood before the end, and I traced both new Rust tests through fold: the first gives End then Start (idle branch reports the start), the second keeps since across the Restart, so since > max(dry, seen) is false and it stays an Update. No blocking issues found.

Non-blocking

  1. rs/moq-net/src/model/follow.rs new End arm: the heuristic leans toward a gap. If a burst of events never lets the cursor return Pending, a prefix that really started before the exact route ended but after that route's last event is still reported as End then Start. That's the safe direction (an extra restart, never a missed one), but the doc comment on poll_next should say outright that it can over-report gaps, so callers don't treat End as proof the path went unserved.
  2. The same arm also catches different-epoch handoffs that used to give Restart (the PR body calls this out). Anything matching on Restart to keep pads or state, like moqsrc in the m0 quest, now sees End then Start in that race instead. Please confirm those consumers handle a Start after End the same way.
  3. rs/moq-cli/src/play/media.rs:86 still plays on Update while idle, and the test an_update_while_idle_plays_the_route (~line 780) describes the race this PR removes. It should still pass, but its comment is now stale. Worth a tracking note or issue so the follow-up doesn't get lost, since the quest file that tracked it is deleted here.
  4. No test covers the Ready(None) path in poll_announced while idle is false (the origin closes mid-handoff). It's a minor gap.
  5. CI is pending on the head.

Verdict: MERGE, once CI is green.

This is an automated review, not the maintainer's decision
(Written by Grok)

@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: b3948db

Reviewed the one-commit update since de1bf47; the base is unchanged.

Fixed: our P2, also independently identified by Codex. In rs/moq-net/src/model/follow.rs:128–164, the replacement's since is compared with the serving route's latest seen and dry. The three-level handoff now stays an Update, and the new Rust/JS regression covers the restart variant without an intervening pending poll. The original coalesced gap still produces End then Start.

No new actionable findings in this update. Direction is sound: per-standing-route sequence state replaces the stale freshness list without adding playback retries or public/wire surface.

Verification: GitHub-only source review and event tracing, not executed tests. The CLI draining regression remains deferred; this is not an end-to-end playback verification.

(Written by OpenAI)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary:

  • Fix: Follow reports End then Start when a same-epoch covering prefix takes over after a gap that the cursor's per-prefix ordering hid. It now tracks take sequence numbers: since and seen per covering route, and dry for the last time the cursor ran dry. A replacement whose start came after both the serving route's last event and the last dry poll may have arrived after that route ended, so its takeover is reported as a gap.
  • Review: OpenAI and Codex both found that the first version's fresh set could outlive a returned Restart or Update. That is fixed in b3948db, with a Rust regression and a JS mirror. OpenAI re-reviewed b3948db and found nothing new. The merge from main after that was clean.
  • CI: the required Check and Test pass on 903d138.
  • API and wire: no new items. follow now reports this gap where it used to report an Update. Wire: none.
  • Deferred to feat!: TS export lingers within an epoch, --stitch switches programs #5147: removing the moq play Update if playing.is_none() workaround, plus a regression that drains through the handoff.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 563be53 into main Oct 10, 2026
9 of 10 checks passed
@kixelated
kixelated deleted the quest/m0/broadcast-epoch/follow-gap branch October 10, 2026 14:50
kixelated added a commit that referenced this pull request Oct 10, 2026
…he gap

With #5188, origin::Consumer::follow reports a gap onto a same-epoch prefix
as an End then a Start, so an Update is always the same instance and the run
rides it out. The weak broadcast handle that only the fallback needed goes too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 10, 2026
…t follow reports gaps

#5188 makes `origin::Consumer::follow` report a gap onto a same-epoch prefix
as an end and a start, so the Follower's workaround goes, as the follow-gap
quest planned. The handoff test keeps its expectation and passes on `follow`
alone. Adds the export-ts quest's regression: a covering prefix starting,
restarting, and ending under a live exact route changes nothing.

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