Skip to content

Publish claim answers under their own epochs - #5678

Merged
kixelated merged 26 commits into
mainfrom
quest/m1/claim-answer-epochs
Oct 21, 2026
Merged

kixelated merged 26 commits into
mainfrom
quest/m1/claim-answer-epochs

Conversation

@kixelated

@kixelated kixelated commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A prefix claim answered every path under the claim's own instance. A worker restarting one output (closing it and answering the next request with a fresh broadcast) therefore looked like the same instance to every hop, so a returning reader could get the old output's cached groups spliced onto the new one.

Approach

  • A claim answers by publishing: Request::prepare(epoch) returns a Prepared holding an unannounced broadcast at the exact path; fill it, then Prepared::publish() announces it under that epoch and resolves the waiters. Dropping it unpublished refuses the request (JS: prepared.reject(err)). prepare withholds the broadcast's announcer until publish, so the answer cannot announce itself under another epoch or skip admission (decision 9).
  • Public claims carry no route: origin.dynamic(prefix) in Rust, JS, C, UniFFI, and every wrapper.
  • Session forwarding keeps its route-carrying claim (dynamic_route / dynamicRoute), its re-pricing (update_route / updateRoute), and accept, as #[doc(hidden)] pub in Rust and @internal in JS (decision 1). An application claim refuses accept (Rust panics, JS throws), so the public API cannot serve two outputs under one instance.
  • Migrate Rust, TypeScript, C, UniFFI, and the Python, Go, Swift, Kotlin, and Dart wrappers and their tests.
  • Tests: hidden until published with its epoch; dropped, abandoned, and released answers are refused and never announced; a pinned request resolves only through the published epoch; accept on an application claim is refused. End to end (tests/integration/restart.rs claim_restart_*): a worker restarts pool/p within the cache window behind two relays on lite-05/06/07 and moq-transport 14/16/22; the returning reader gets the new output and pool/q keeps its reader. Swapping the worker back to the old accept path fails that test with "joined the old output".

Decisions

Decided by the maintainer; ✅ marks the choice.

  1. Peer-route escape hatches (dynamic_route, update_route, accept; JS dynamicRoute, updateRoute, accept): ✅ keep them #[doc(hidden)] pub / @internal as the settled interim / make them private in this PR. Their callers model a peer's route (a covering claim with hops, cost, or an epoch, answered with a broadcast the session already routes), which has no public shape yet. Making them private is recorded in Public routes and answering, which owns that public shape, and the session-reached JS members carry its // pending: mark.
  2. Claim cost: ✅ claims carry no cost; an application drains by dropping the claim or draining its session / add a claim config with only cost.
  3. Cold-start reset: a relay resolves a claim path optimistically, so the first subscription through it is refused once the worker's exact route arrives; readers that follow announcements recover. ✅ accept until announce takeover lands / block on takeover.
  4. Atomic publish (Rust): ✅ fix in this PR. Prepared::publish admits the request and lands the route under the origin's table lock, which every release (a new epoch, a retraction, the teardown) takes first, so an answer is never announced and then withdrawn. The loom model a_refused_answer_is_never_announced (just rs loom, nightly) fails on the old check-announce-resolve sequence with "a refused answer was announced".
  5. JS abandonment: JS has no destructors. ✅ Prepared.reject(err) / Symbol.dispose.

Taken while merging main (the recommended option in each case, no maintainer call needed):

  1. JS Dynamic.update: it was still public, so an application claim could take an epoch in JS. ✅ rename it to an internal updateRoute, mirroring Rust's hidden update_route / leave it public.
  2. Main's new FFI test of a covering same-epoch route (announced_broadcast_hands_out_the_instance_again_after_an_end): ✅ keep its scenario on the hidden peer route through the inner origin / rewrite it onto prepare, which publishes a new instance and so no longer tests the same-instance hand-out.
  3. Upgrade note: main moved breaking changes into per-package MIGRATE.md. ✅ one entry per affected package (moq-net, @moq/net, moq-ffi, moq-c, Python, Go, Swift, Kotlin, Dart, C++) / keep a cross-package line in doc/setup/upgrade.md, which now covers only wire ordering.

Decided by the maintainer on review:

  1. Self-announcing answers (Codex finding): Prepared::broadcast() returned a full producer, so a handler could announce() the answer under any epoch and skip publish()'s admission. ✅ withhold the announcer: prepare takes the announcer slot, announcing before publish() fails with Closed (JS throws), and a refused publish() drops the announcer so no clone can announce it later / expose a fill-only handle type. No new public type; the bindings inherit it from the Rust producer.

Impact

Public API: breaking.

  • Rust: origin::Producer::dynamic(prefix) takes no route; new origin::Request::prepare(epoch) and origin::Prepared::{broadcast, publish}; Dynamic::update and Request::accept hidden (accept panics on an application claim). Prepared::broadcast()'s own announce fails with Closed until publish (JS: prepared.broadcast.announce() throws; the bindings' announce errors with Closed).
  • JS: Producer.dynamic(prefix); new Request.prepare(epoch) and Prepared { broadcast, publish(), reject(err) }; Dynamic.update becomes the internal updateRoute; accept is internal and throws on an application claim.
  • moq-ffi / bindings: dynamic(prefix), MoqBroadcastRequest.prepare(epoch), MoqBroadcastPrepared { broadcast, publish } (aliased BroadcastPrepared in Python, Swift, Kotlin, Dart, Go; generated as BroadcastPrepared in C++); accept and MoqOriginDynamic.update removed.
  • C: moq_origin_dynamic takes no route; moq_origin_dynamic_update and moq_broadcast_request_accept removed; moq_broadcast_request_prepare, moq_broadcast_prepared_publish, moq_broadcast_prepared_free added, as the C freeze's narrow claim exception.

Wire impact: none.

Checks

  • just check (branch against main, at the final head): passed. One run hit the known redundant_importers_switch_through_bufferbloat_on_lite_07 flake (quest); the rerun passed.
  • just rs loom a_refused_answer (the publish race model, after withholding the announcer): passed.
  • Earlier rounds: just check passed, including Rust, JS, Go, Python, Kotlin, Dart, C++, docs, and the model-marker lint. Swift was skipped (no Swift toolchain on this Linux host).
  • just test interop --all (final head): passed, 74 PASS, 0 FAIL. Earlier round: Rust protocol interop 5/5, the Rust, Python, Go, C++, and JS publisher matrix, browser close code, CLI FETCH close, finite track tails, and the drop cases (74 PASS, 0 FAIL).
  • quest check: passed.

Follow-ups

  • Make the peer-route escape hatches private once a public peer-route shape exists: recorded in Public routes and answering (decision 1).
  • Same-epoch lagging replica resume, from the quest's verification list. Main now carries epochs over moq-transport too (moq-dev/moq#5932), so it can be checked on IETF as well as lite-07.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 15, 2026 21:46
@kixelated

Copy link
Copy Markdown
Collaborator Author

Implemented the claim answer migration across Rust, TypeScript, C, UniFFI, and hand-written wrappers.

Verified:

  • cargo check --all-targets -p moq-net -p moq-ffi -p moq-c
  • cargo test -p moq-net claim_answer
  • bun run --filter @moq/net check
  • Kotlin UniFFI bindings regenerated

The PR remains draft because the full cross-language interop matrix and broader relay restart coverage have not run yet.

(written by GPT-5.4)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Latest-main merge and follow-up fixes are pushed in 2092e8a.

Validation:

  • just check: all code, lint, docs, feature, and test suites passed; its final quest metadata step found stale links to the completed quest document, which this commit fixes.
  • quest check: 454 documents passed after that fix.
  • just test interop --all: Rust protocol interop passed 5/5 and non-browser cross-language pairings passed. Browser cases were environment-blocked before exercising product code because the cached Playwright Chromium cannot load libglib-2.0.so.0 in this host/Nix runtime; using Nix library paths instead exposes a host/Nix glibc ABI mismatch. CI should provide the authoritative browser result.
  • git diff --check: passed.

The PR remains draft while CI runs. Public API: breaking claim API migration across Rust, JS, FFI, C, and wrappers. Wire impact: none.

(written by GPT-5.4)

kixelated and others added 9 commits October 16, 2026 14:39
Remove the duplicate Dynamic::update shim, panic when a Rust application
claim is answered with accept, keep JS test claims originated, add
Prepared.reject in JS, cover abandoned, released, and pinned answers, and
fix the C++, Python, and FFI CI failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the claim upgrade note to the unreleased section, regenerate the Dart
bindings, sort the Python imports, and migrate the claims main added since.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A worker claims pool through the public API and restarts pool/p within the
cache window; the returning reader at the edge gets the new output, never
the old one, and pool/q keeps its reader. The same test with the old accept
path joins the old output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python, Go, Swift, Kotlin, and Dart tests still answered claims with accept
and passed a route; the Python wrapper still exposed OriginDynamic.update.

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

# Conflicts:
#	quest/m1/claim-answer-epochs.md
Prepared::publish checked the request, announced, then resolved, rolling
back if the request was released in between, so a concurrent release could
announce the answer and withdraw it again. Admit the request and land the
route under the origin's table lock, which every release takes first.

A loom model fails on the old sequence. Default-route claims in benches
and the rtc test use the public dynamic(prefix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 3 commits October 20, 2026 13:18
…-epochs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirror Rust's hidden update_route in JS as an internal updateRoute, mark
the session-reached accept and updateRoute as pending the public answer
quest, keep model-internal visibility for the atomic announce, move the
upgrade note into each package's MIGRATE.md, and refresh the claim
paragraph in the moq-lite concept page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 20, 2026 20:52
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

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 6 minutes.

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: 6867ad47-a08b-4d65-8ea7-a82de3465406
📥 Commits

Reviewing files that changed from the base of the PR and between 7d2a91f and f187ebe.

📒 Files selected for processing: 78
  • cpp/moq/MIGRATE.md
  • dart/moq/MIGRATE.md
  • dart/moq/lib/src/aliases.dart
  • dart/moq/test/moq_test.dart
  • dart/moq_ffi/lib/src/moq.dart
  • doc/concept/moq-lite.md
  • doc/lib/js/net.md
  • doc/lib/rs/index.md
  • go/wrapper/MIGRATE.md
  • go/wrapper/moq_test.go
  • go/wrapper/origin.go
  • go/wrapper/origin_internal_test.go
  • js/net/MIGRATE.md
  • js/net/bench/broadcasts.ts
  • js/net/bench/requests.ts
  • js/net/src/broadcast.ts
  • js/net/src/connection/forward.ts
  • js/net/src/connection/pool.test.ts
  • js/net/src/ietf/publisher.test.ts
  • js/net/src/integration.test.ts
  • js/net/src/internal.ts
  • js/net/src/lite/publisher.test.ts
  • js/net/src/origin-demand.test.ts
  • js/net/src/origin-request.test.ts
  • js/net/src/origin.test.ts
  • js/net/src/origin.ts
  • js/net/src/untimed.test.ts
  • js/watch/src/broadcast.test.ts
  • kt/moq/MIGRATE.md
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt
  • kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt
  • py/moq-rs/MIGRATE.md
  • py/moq-rs/README.md
  • py/moq-rs/moq/__init__.py
  • py/moq-rs/moq/origin.py
  • py/moq-rs/tests/test_local.py
  • py/moq-rs/tests/test_server.py
  • quest/m1/README.md
  • quest/m1/announce-takeover.md
  • quest/m1/c/README.md
  • quest/m1/claim-answer-epochs.md
  • quest/m1/public-model/answer.md
  • rs/moq-c/MIGRATE.md
  • rs/moq-c/src/api.rs
  • rs/moq-c/src/origin.rs
  • rs/moq-c/src/test.rs
  • rs/moq-cli/src/play/media.rs
  • rs/moq-ffi/MIGRATE.md
  • rs/moq-ffi/src/origin.rs
  • rs/moq-ffi/src/producer.rs
  • rs/moq-ffi/src/test.rs
  • rs/moq-gst/src/source/imp.rs
  • rs/moq-hls/src/export/mod.rs
  • rs/moq-mux/benches/containers.rs
  • rs/moq-mux/benches/export.rs
  • rs/moq-mux/src/catalog/hang/consumer.rs
  • rs/moq-mux/src/container/consumer.rs
  • rs/moq-mux/src/container/ts/export_test.rs
  • rs/moq-mux/src/source.rs
  • rs/moq-net/MIGRATE.md
  • rs/moq-net/benches/origin.rs
  • rs/moq-net/benches/retirement.rs
  • rs/moq-net/benches/viewers.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/broadcast.rs
  • rs/moq-net/src/model/follow.rs
  • rs/moq-net/src/model/mod.rs
  • rs/moq-net/src/model/origin.rs
  • rs/moq-net/tests/integration/drained_claim.rs
  • rs/moq-net/tests/integration/restart.rs
  • rs/moq-net/tests/loom.rs
  • rs/moq-rtc/src/egress.rs
  • swift/MIGRATE.md
  • swift/Sources/Moq/Origin.swift
  • swift/Tests/MoqTests/SmokeTests.swift
  • 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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 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-21T00:42:51.444337Z 3d7f99c New commits
ℹ️ 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.

@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: 275b8d55fc

ℹ️ 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 js/net/src/origin.ts Outdated
Comment on lines +2462 to +2463
this.#done = true;
return this.#request.publishPrepared(this.broadcast, this.#epoch);

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 Keep failed prepared answers rejectable

Set #done only after publishPrepared succeeds, or clean up on failure. If the prepared broadcast was closed or superseded while tracks were being populated, answer.announce() throws, but this assignment makes the subsequent reject() a no-op. The claim's request then remains pending indefinitely, so all coalesced requesters hang until the entire claim closes.

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.

Fixed in cb893b20c4: a failed Prepared.publish() now closes the broadcast and refuses the request before rethrowing, matching Rust where the dropped Prepared releases it. Regression test a prepared answer that fails to publish refuses the request fails without the fix.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 20, 2026 15:04
Matches Rust, where a failed Prepared::publish drops the request and
resolves its waiters instead of leaving them pending.

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

@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: 09c208e442

ℹ️ 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 go/wrapper/origin.go
Comment on lines +127 to +131
// Publish announces the answer and resolves every waiting requester.
func (p *BroadcastPrepared) Publish() (*BroadcastProducer, error) {
inner, err := p.inner.Publish()
if err != nil {
return nil, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Expose a deterministic way to abandon prepared answers

When a Go handler successfully calls Prepare but then fails while populating tracks, it can no longer call BroadcastRequest.Reject, while BroadcastPrepared exposes only Broadcast and Publish and hides the generated handle's Destroy. The Rust request therefore remains pending until Go's nondeterministic finalizer runs, potentially leaving every coalesced requester hanging indefinitely. Add a Close/reject operation that synchronously releases the prepared handle.

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.

Fixed in bd965eb665: BroadcastPrepared.Close() destroys the handle synchronously, dropping the Rust Prepared and refusing every waiting requester. The other bindings already had a deterministic path (C moq_broadcast_prepared_free, uniffi close()/destroy() on Kotlin and Dart, ARC in Swift, refcounting in Python); Go was the only one hiding it. Regression test TestClosingAPreparedAnswerRefusesTheRequest times out without the call.

(Written by Claude Opus 5.5)

Prepare consumes the request, so a handler that fails while filling tracks
could no longer refuse it; the request stayed pending until Go's finalizer
freed the handle. Close releases it now, refusing every waiting requester.

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

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

ℹ️ 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/origin.rs Outdated
Comment on lines +4985 to +4987
/// The unannounced broadcast to fill before publishing.
pub fn broadcast(&self) -> &broadcast::Producer {
&self.broadcast

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 Prevent prepared broadcasts from announcing directly

When a handler calls prepared.broadcast().announce(...) before Prepared::publish (the bindings expose the same operation), this accessor returns an unrestricted producer whose public announce bypasses both the request-admission check and the epoch stored by prepare. It can therefore publish the answer under an arbitrary or missing epoch, even after the claim/request was released, so abandoning the Prepared no longer guarantees that the answer stayed hidden and the cache-splicing problem this change addresses can recur. Expose a fill-only handle or withhold the announcer until publish so this misuse is unrepresentable.

AGENTS.md reference: AGENTS.md:L66-L66

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.

Fixed in 7fe2441d46 by withholding the announcer, with no new public type: prepare takes the broadcast's announcer slot and Prepared holds it, so prepared.broadcast().announce(..) fails with Error::Closed (JS: throws) and route() stays empty. publish hands the announcer back under the same lock as the admission check, and installs it only if the request is admitted and the broadcast is still open. On a refused publish the announcer is dropped, so a cloned producer can't announce the answer afterwards either. The bindings (moq-ffi, C, and the wrappers) wrap the same Rust producer, so they get this behavior automatically.

Regression tests that fail without the fix: a_prepared_answer_cannot_announce_itself and a_refused_prepared_answer_cannot_announce_later (moq-net), a prepared answer cannot announce itself before publish (js/net), plus an assertion in moq-ffi's dynamic_broadcast_request. Each checks that announcing before publish fails and that publish still announces under the prepared epoch.

(Written by Claude Opus 5.5)

`Prepared::broadcast()` handed out a full producer, so a handler could
announce the answer itself under any epoch and skip publish's admission.
`prepare` now takes the announcer slot: announcing before `publish` fails
with `Closed`, and a refused publish drops the announcer for good. The JS
`Prepared` does the same; the bindings inherit it from moq-net.

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

@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: 3d7f99cf12

ℹ️ 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 js/net/src/origin.ts
prepare(epoch: Epoch.Valid): Prepared {
if (this.#done) throw new Error("request is closed");
if (!this.publisher) throw new Error("forwarded requests cannot be prepared");
this.#done = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject prepared answers when the handler unwinds

When an exception occurs after prepare() but before an explicit publish() or reject(), such as while populating tracks, this marks the request done, so Dynamic.requested()'s finally block calls current.reject(...) but that method immediately returns. Because Prepared has no Symbol.dispose or other automatic cleanup, the entry remains pending and every coalesced requester waits until the entire claim closes. Give Prepared scoped cleanup or keep the request rejectable until the prepared answer settles.

AGENTS.md reference: AGENTS.md:L66-L66

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.

Declining, because this reopens a decision the maintainer already made. Decision 5 in the description kept JS abandonment explicit (Prepared.reject(err)) over scoped cleanup (Symbol.dispose), and js/net/MIGRATE.md documents that an abandoned Prepared holds its request open. A handler whose fill can throw should catch the error and call prepared.reject(err).

Having the iterator's finally reject an outstanding Prepared would also break the match with Rust. There, prepare consumes the Request, and the Prepared that results lives on its own, independent of the claim's request stream. A handler that prepares, hands the fill to another task, and then leaves the loop would have that answer refused out from under it. That case shows up only on the JS side.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary

  • Codex P2, self-announcing answers (thread): fixed as the maintainer decided, by withholding the announcer. prepare takes the broadcast's announcer slot. Announcing the prepared broadcast before publish() now fails with Closed (JS throws), and a refused publish() drops the announcer. No new public type. moq-ffi, C, and the wrappers wrap the same Rust producer, so they inherit this. Commit: 7fe2441d46. Regression tests in moq-net and js/net (plus an assertion in moq-ffi) fail without the fix.
  • Codex P1, JS unwinding (thread): declined, because it reopens decision 5 (explicit reject(err) rather than scoped cleanup) and would break the match with Rust's owned Prepared.
  • Merged main with a merge commit. Ran just dart generate for the doc-comment checksum.
  • Checks on head 3d7f99cf12:
    • just check passed. The first run hit the known flake in quest/m1/test-flakes-2/redundant-failover-drift.md; the rerun passed.
    • just test interop --all passed: 74 PASS, 0 FAIL.
    • The loom model a_refused_answer_is_never_announced passed.
  • Codex reviewed head 3d7f99cf12.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 21, 2026
kixelated and others added 3 commits October 20, 2026 20:15
…im-answer-epochs

# Conflicts:
#	cpp/moq/MIGRATE.md
#	dart/moq/MIGRATE.md
#	go/wrapper/MIGRATE.md
#	kt/moq/MIGRATE.md
#	py/moq-rs/MIGRATE.md
#	rs/moq-c/MIGRATE.md
#	rs/moq-ffi/MIGRATE.md
#	rs/moq-net/MIGRATE.md
#	swift/MIGRATE.md
The follow and front-reconcile tests from #5925 used dynamic(prefix, route). Unanswered default-route claims become dynamic(prefix); those that accept a peer's broadcast or carry an epoch keep their scenario on the hidden dynamic_route.

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

Copy link
Copy Markdown
Collaborator Author

Re-enqueue after the merge-queue conflict

  • Merged main twice with merge commits: f5419ed3ea and 9c1874231f.
  • The text conflicts were all in MIGRATE.md files: the moq-net, moq-ffi, moq-c, C++, Go, Python, Swift, Kotlin and Dart entries for this PR and for feat(moq-net): time anchors for subscription ranges #6074. I kept both entries in each.
  • The tests added by feat(net): follow announced instances in moq-net #5925 called the old dynamic(prefix, route). cbffa94e61 moves them onto the new API:
    • The three claims that never answer use a default route, so they now use dynamic(prefix).
    • The three that accept a peer's broadcast or carry an epoch keep their scenario on the hidden dynamic_route, the same treatment decision 7 gave earlier.
    • Only test code changed.
  • Checks on head 9c1874231f:
    • just check passed.
    • just test interop --all passed on cbffa94e61 (74 PASS, 0 FAIL). The second main merge touched no wire or moq-ffi code.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 21, 2026
…im-answer-epochs

# Conflicts:
#	go/wrapper/MIGRATE.md
#	rs/moq-c/MIGRATE.md
#	rs/moq-ffi/MIGRATE.md
@kixelated

Copy link
Copy Markdown
Collaborator Author

Re-enqueue after the second merge-queue conflict

  • Merged main at 7d2a91fe with a merge commit: f187ebe44b.
  • The only conflicts were in the Go, moq-c and moq-ffi MIGRATE.md files: this PR's entry next to moq-net: compressed tracks #5928 compress entry. I kept both.
  • No semantic breaks: this PR's tests have no track-info literals that the new compress field affects, and main's new tests use the claim API only as dynamic(prefix). No code changed in the merge.
  • Checks on head f187ebe44b:
    • just check passed.
    • just test interop --all passed (74 PASS, 0 FAIL).

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 21, 2026
@kixelated
kixelated added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 964b5ae Oct 21, 2026
10 checks passed
@kixelated
kixelated deleted the quest/m1/claim-answer-epochs branch October 21, 2026 16:04
kixelated added a commit that referenced this pull request Oct 21, 2026
Drop claim-answer-epochs (landed in #5678) and point replica-resume at the merged PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 21, 2026
main's #5678 makes a dynamic claim answer by publishing, so the tests that
need a covering prefix route serving on request use the internal route.

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