Skip to content

feat(net)!: prove cache-aware routing settles under fresh seeds - #8732

Merged
kixelated merged 5 commits into
mainfrom
quest/m2/cache-routing-proof
Nov 14, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m2/cache-routing-proof

Conversation

@kixelated

@kixelated kixelated commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

Completes quest/m2/cache-routing-proof.md: cache-aware routing's warm routes now settle under every fresh seed tried. The proof is rebuilt on the real route::path::Router from moq-dev/moq#8622, the paths.rs prototype is deleted, the long sweep runs nightly, and lite-07's warm fields carry the new rule. The quest file is deleted; the epic's item 14 records the rule.

What changed

  • The rule (rs/moq-net/src/route/tests/cache.rs), in three layers that each settle before the next depends on them:
    • Routes are path::Router's, which copies never change.
    • Offers. A relay with a local reader is its copy's carrier. It offers every neighbour its copy at no cost, whatever it pulls. A relay with no reader offers each neighbour the best of its route and the copies offered to it whose hops name neither relay, when that best is a copy. Copies rank as routes do: drain, then priority, then cost. An offer depends only on readers, routes, and other offers, never on who pulls what, so offers settle as a monotone path vector does. steady() checks the distributed offers against a fixed point computed centrally.
    • Pulls. A carrier pulls through its best source among its route and the copies of carriers ranked below it by the Rendezvous Hash, whoever pulls from it. A relay with no reader pulls through its best source whose hops name no neighbour it serves, so long as that is, for each such neighbour, the copy it offered it or a lower-ranked carrier's; otherwise it follows its route. SUBSCRIBE's new Warm byte says whether a pull joins the copy offered or follows the route, and a relay pulled along its route follows its own.
    • Loop freedom once quiet. Every join gets a copy whose carrier, then whose rank, is strictly better than what it gives each relay joined to it, and a pull along routes follows the routes' tree.
  • The sim:
    • It now runs on path::Router with per-link priorities and drains, two publishers, and six relays.
    • A lingering pull keeps its parent and runs out on a 30 s timer (track::IDLE_LINGER), even while the mesh is busy.
    • A failed settle prints a dump plus a delivery trace.
    • Sim::now() was added.
  • Fixtures, all 15 passing. New: a_pull_along_the_route_stays_on_the_route and a_copy_ranks_by_priority_before_cost. The unranked pair in equal_relays_pull_one_copy_owned_by_the_lower_hash now settles into a cycle that feeds neither, which shows why the rank is needed.
  • Wire (moq-lite-07):
    • WARM_START and WARM_UPDATE carry the Warm Cost, a Flags byte (0x1 marks a draining copy), a priority (i8), and the Hops, carrier first and sender last. These replace the Carrier Index.
    • A hop count of 0 (no carrier) or more than 32, or an unknown flag, is refused as moq-dev/moq#8672's Refused Content: the receiver drops that warm route and keeps the session.
    • SUBSCRIBE and SUBSCRIBE_UPDATE end with a Warm byte (0 = follow the route, 1 = join the copy offered; above 1 is a violation).
    • The draft's Warm Routes section states the rule and the loop-freedom argument.
    • The Rust codec (lite/announce.rs, lite/subscribe.rs) and js/net (lite/subscribe.ts, lite/version.ts) match. Both send Warm = 0 and ignore it when read, until the implementation.
  • Nightly: test/route-sweep.sh runs sweep_cache_routing_long from its fresh MOQ_ROUTE_SEED, and its line in sh/gh/test-inventory.allow is gone.
  • Docs:

Validation

  • Long sweep under 12 seeds (MOQ_ROUTE_SEED = 3401901550, 305419896, 1 to 8, 1777, 2026111401): 72,000 scenarios, 3000 each with and without held links per seed, all settle. That covers every seed that failed under feat(net)!: prove propagating warm routes on paths, and carry their hops, cost, and carrier in moq-lite-07 #8615's rules, and every scenario this rule failed while I was developing it. Each failure was traced by hand from the sweep's dump and delivery trace before its rule changed. Settled means: each reader is fed, subscriptions and offers match their senders, the distributed offers equal the fixed point computed centrally, no pull lingers, and nothing changes on a further tick.
  • Warm messages per demand change: about 3.1 per reader change (1,689,309 for 540,583 with held links, 1,717,475 for 540,058 without). The sweep asserts that an idle broadcast sends none.
  • Transient cycles during churn, which the maintainer accepted on 2026-11-13 until the crossing change is delivered:
  • quest gate cargo nextest run -p moq-net --lib -E 'test(/route::tests::cache/)': 15 passed, including the gate sweep (100 scenarios with held links and 100 without).
  • Codec tests: lite07_warm_route_layout (byte for byte), lite07_warm_route_hops_and_flags_are_bounded, and warm_trails_subscribe_only_on_lite07; js/net's subscribe.test.ts.
  • quest gate just check passes, before and after merging main (13,142 Rust tests after), plus js/net's tests, drafts, and quest checks. quest gate just test interop --all passes on both, Rust, JS, and Python on moq-lite-07-wip included. just fix changed nothing in this diff; its biome step failed while installing js/libav (the Nix daemon disconnected), so I ran biome on the three changed JS files directly: no fixes.

Decisions

All taken by the agent under /quest-afk, for the maintainer's review:

  • Offers that do not depend on demand, offered per neighbour. A carrier offers its copy at a constant 0. A relay with no reader offers each neighbour its best source excluding copies that name either relay. Rejected:
  • A carrier's pull ignores who it serves; a relay with no reader may pull a lower-ranked carrier's copy instead of the one it offered. Both cut the feedback that kept the last flips going: whom a relay serves changed what it could pull, which changed whom it served. Rejected: binding a relay to exactly the copy it offered each neighbour, which flipped seed 3401901550 scenario 2326 for good.
  • A pull with no demand lingers on its parent for track::IDLE_LINGER (30 s) on a timer, and the pulls around it may move until that runs out. A lingering pull can hold a copy its parent promised another neighbour; all the scenarios still unsettled after the rules above (4 in 48,000) were such a pull and its neighbours flipping. settle stops counting busy rounds while a linger is pending, for up to four lingers, so "settled" means once demand, lingers included, stops changing. Rejected: expiring lingers only once the mesh is quiet, which reported those 4 as never settling; dropping a pull with no demand at once, which throws away the linger's purpose (a reader that comes back finds the copy warm); a rule that moves a lingering pull, since it has no demand to rank sources by.
  • The proof runs on path::Router, and paths.rs is deleted. Rejected: keeping the prototype beside the router that ships.
  • Wire: drain and priority travel in warm routes, and the carrier is the first hop. Rejected: keeping Carrier Index; ranking copies by cost alone, which ignores what a route's own rank already says.
  • SUBSCRIBE carries Warm on both SUBSCRIBE and SUBSCRIBE_UPDATE. Rejected: inferring it from the offer, which a crossing WARM_UPDATE makes ambiguous; carrying it on SUBSCRIBE only, which forces a new stream to switch between the copy and the route.
  • Transient loops are accepted at this rule's rate, which Pullfrog asked to decide explicitly rather than read into the 2026-11-13 acceptance (made at feat(net)!: prove propagating warm routes on paths, and carry their hops, cost, and carrier in moq-lite-07 #8615's 0.39% of churn steps and 53 deliveries).
    • The rate here is 3.3% of churn steps, each cycle gone within 142 deliveries, and about 9 in 10 such steps leave some reader unfed until it clears.
    • Seed 3 at 1000 scenarios per mode ruled out the two obvious suspects. Baseline: 3.1 to 3.3%. Without per-link priorities: 2.8 to 3.0%. With carriers excluding neighbours they serve, as feat(net)!: prove propagating warm routes on paths, and carry their hops, cost, and carrier in moq-lite-07 #8615 did (which leaves scenarios that never settle): 2.9 to 3.1%.
    • So neither explains the gap. It is this sweep's churn or the rule's per-neighbour offers, and the two sweeps count steps differently.
    • Every cycle clears once the crossing message lands, and a relay's pull is never wrong at quiescence.
    • Rejected: holding the PR to chase the rate down. Whatever would lower it, for example a carrier waiting for a crossing message before it joins, adds latency to every join to save transient cycles, and that trade is the maintainer's.
    • Decided 2026-11-14 (maintainer, /quest-complete): ✅ merge, and plan a quest to lower the rate (rejected: accept the rate as is, hold the PR, delete).
  • No TLA+ model of the copy choice. Each layer's argument is a monotone path vector or a strict lexicographic order, and the sweeps settle under every seed. Rejected: modelling it in TLA+ beside RouteHandoff.tla, which the quest suggested only while scenarios still failed.

Public API and wire impact

No public API change: the codec lives in the private lite module.

Wire: moq-lite-07 only, which is unpublished (moq-lite-07-wip). WARM_START and WARM_UPDATE replace Carrier Index with a Flags byte and a priority, and SUBSCRIBE and SUBSCRIBE_UPDATE gain a trailing Warm byte. Migration: peers on an earlier moq-lite-07-wip build of main must upgrade together. The release branch carries none of these fields.

Caused by

Not a bug fix: moq-dev/moq#8615 left scenarios under fresh seeds that never settled, and moq-dev/moq#8622 landed the router to build the proof on.

Follow-ups

  • A quest to lower the transient-cycle rate during churn (3.3% of steps, up to 142 deliveries), planned with /quest-plan after this lands, per the maintainer's 2026-11-14 pick.
  • The implementation acts on the warm messages and the Warm byte.

Models

Size: L
Implementer: Claude Code, claude-opus-5-5, unknown effort

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

Review

Rewritten by quest review after each round.

  • Head: a7ce45d

  • Base: origin/main at 8088d55

  • Reviewer: Codex, gpt-6.1-sol, xhigh effort

  • Session: 01a1d65d-060c-7f70-9cdb-787a0d3dc76f, round 4

  • Score: 10/10, The draft clarification matches the existing routing model, and the merge introduces no actionable regressions; builds, tests, and model checks were not run as instructed.

  • Rounds: 5; findings 0: fixed 0, refuted 0, open 0

  • Findings: none

kixelated and others added 2 commits November 14, 2026 03:12
Rebuild the cache-routing proof on route::path::Router and settle every
scenario of the long sweep under 12 seeds (72,000 scenarios):

- Offers do not depend on demand: a carrier offers its copy at no cost,
  and a relay with no reader offers each neighbour its best source whose
  hops name neither relay.
- A carrier pulls its best source among its route and lower-ranked
  carriers' copies, whoever pulls from it. A relay with no reader pulls
  what it offered each neighbour it serves, or a lower-ranked carrier's
  copy, else its route.
- A lingering pull runs out on IDLE_LINGER's timer even while busy.

Lite-07: WARM_START and WARM_UPDATE carry Flags (draining) and a
priority, with the carrier as the first hop, in place of Carrier Index.
SUBSCRIBE and SUBSCRIBE_UPDATE end with a Warm byte. The Rust and js/net
codecs match.

The long sweep runs nightly from test/route-sweep.sh. Delete the paths.rs
prototype and the completed quest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Prove cache-aware routing feat(net)!: prove cache-aware routing settles under fresh seeds Nov 14, 2026
@kixelated
kixelated marked this pull request as ready for review November 14, 2026 13:52
@coderabbitai

coderabbitai Bot commented Nov 14, 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 20 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

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 17ef82b3-b1c2-450f-b593-e191f9eefadb
Commits

Reviewing files that changed from the base of the PR and between bd617cc and a7ce45d.

Files selected for processing: 20

  • .claude/skills/verify-route/SKILL.md
  • drafts/draft-lcurley-moq-lite.md
  • js/net/src/lite/subscribe.test.ts
  • js/net/src/lite/subscribe.ts
  • js/net/src/lite/version.ts
  • quest/m2/cache-routing-proof.md
  • quest/m2/cluster-routes/route-exhaustive-sim.md
  • quest/m2/cluster-routes/switch.md
  • quest/m2/lite07-finalize.md
  • quest/m3/cache-routing/README.md
  • quest/m3/cache-routing/select.md
  • quest/m3/route-trace-validation.md
  • rs/moq-net/src/lite/announce.rs
  • rs/moq-net/src/lite/subscribe.rs
  • rs/moq-net/src/lite/version.rs
  • rs/moq-net/src/route/tests/cache.rs
  • rs/moq-net/src/route/tests/mod.rs
  • rs/moq-net/src/route/tests/paths.rs
  • sh/gh/test-inventory.allow
  • test/route-sweep.sh
  • 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

# Conflicts:
#	drafts/draft-lcurley-moq-lite.md
#	quest/m3/cache-routing/select.md
#	rs/moq-net/src/lite/announce.rs

@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 critical issues. Two small things inline, plus one tradeoff for the maintainer to decide.

Reviewed changes

I read the full diff at 4866726: the lite-07 wire reshaping in the draft, the Rust and JS codecs, the rebuilt cache.rs proof, and the nightly and quest wiring. paths.rs is a pure deletion, and nothing still references it.

  • Warm route wire: WARM_START/WARM_UPDATE replace Carrier Index with Flags (8) and Priority (8), and Hops now run carrier-first. The Rust encode_warm/decode_warm and the byte-exact layout test agree with the draft.
  • Warm byte on SUBSCRIBE and SUBSCRIBE_UPDATE: Rust and js/net both write 0, check that a received value is ≤1 and otherwise ignore it, and gate the byte on has_warm/hasWarm. No other in-repo encoder of lite SUBSCRIBE needs updating, because moq-ffi goes through Rust.
  • Draft Warm Routes section: offers no longer depend on pulls, and pulls follow a three-layer rule. The ranking (drain, then priority, then cost, then warm, then the lower carrier hash) matches Mesh::pick/best/decide.
  • Proof sim: Mesh now runs on path::Router directly. Lingers run out on a timer, and steady checks the distributed offers against fixed_point.
  • Nightly: test/route-sweep.sh runs sweep_cache_routing_long at 3000 cases under the sweep profile, from the script's fresh MOQ_ROUTE_SEED.

ℹ️ Transient cycles are now much more frequent, and most of them starve a reader

The maintainer accepted transient loops on 2026-11-13 at #8615's measured rate: 0.39% of churn steps, gone within 53 deliveries. This rule produces a cycle after 3.3% of churn steps (about 8× as often), each lasting up to 142 deliveries, and about 9 in 10 of those steps leave some reader unfed. The PR says the sweeps aren't directly comparable and that nobody dug into the difference. That is fair, but approving the move to the new rule also accepts these numbers, so the maintainer should decide it explicitly rather than assume it falls under the earlier acceptance.

Technical details
# Transient-cycle cost of item 14's rule vs #8615's

## Affected sites
- rs/moq-net/src/route/tests/cache.rs `sweep` — reports `loops`, `steps`, `looped`, `starved`, `longest`
- quest/m3/cache-routing/README.md item 14 — records 3.3% / 142 deliveries

## Required outcome
- Either an explicit maintainer acceptance of the higher transient rate and starvation, or a comparison that attributes the increase (e.g. run #8615's sweep config, with no per-link priorities, against this rule) so the follow-up is scoped.

## Open questions for the human
- Is ~3% of churn steps with a reader starved until the crossing message arrives acceptable for the implementation quest (`quest/m3/cache-routing/select.md`), or should a follow-up quest own reducing it?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread drafts/draft-lcurley-moq-lite.md Resolved
Comment thread sh/gh/test-inventory.allow Resolved

@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.

ℹ️ The merge resolution introduces no new issues. The two inline notes from the previous review are still open, since this push doesn't touch either line.

Reviewed changes

Since 4866726, the only new commit merges origin/main. Its conflict resolution adapts this PR's warm route codec to #8672's Refused Content.

  • Warm route refusals: decode_warm now returns Content<Warm>. An unknown flag, 0 hops, or more than 32 hops is refused with Refusal::Flags, Refusal::Carrier, or Refusal::Hops, and the rest of the body is skipped. Before, each of these was a DecodeError. Each refusal fires before a later field is read, so skipping the rest of the body cannot desync the stream.
  • Refusal::Carrier: this variant replaces main's "carrier not among its hops" case, which no longer exists because the carrier is now the first hop. Its message now reads "a warm route with no hops, so no carrier".
  • Draft: the Refused Content field list names the warm route's Flags instead of Carrier Index. The WARM_START field rules say "refused" instead of PROTOCOL_VIOLATION, which matches the Rust decoder.
  • Test: lite07_warm_route_hops_and_flags_are_bounded now asserts AnnounceBroadcast::Refused { message: Refusable::WarmUpdate { id: 7 }, .. } for each case. Its byte vectors match the new layout, including the 32-hop boundary.
  • Quest links: the merge carried the removed cache-routing-proof.md link into quest/m3/route-trace-validation.md, where main moved that file. Nothing in the repo still references cache-routing-proof.md or paths.rs.

Pullfrog  | Fix it ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

kixelated and others added 2 commits November 14, 2026 06:47
Address Pullfrog's review: the draft now says which reading the model
takes, and the test inventory drops a header with nothing left under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pullfrog[bot]
pullfrog Bot previously approved these changes Nov 14, 2026

@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 6f5d756, one new commit addresses both prior review threads.

  • Draft pull rule: the Warm Routes text now bounds a relay's pull only by neighbours it offered a warm route, and says a neighbour it offered none (e.g. one whose SUBSCRIBE crossed a WARM_END) bounds nothing. This matches the sim's decide.
  • Test inventory allowlist: the # Too slow for the merge gate… comment is now attached to the sweep_cache_routing_long entry, and the stream-credit entry has its own comment.

Pullfrog  | View workflow run | Using claude-sonnet-5-5 | 𝕏

@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 18f7e4c, the only new commit merges origin/main (8088d55). It doesn't change the routing rule, the sim, either codec, or the draft.

  • Merged main into the branch: main had already dropped the stream-credit entry from sh/gh/test-inventory.allow, so this PR's change to that file now only removes the sweep_cache_routing_long block. Nothing in the repo still references cache-routing-proof.md or paths.rs.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@kixelated
kixelated added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit a6ba6aa Nov 14, 2026
10 checks passed
@kixelated
kixelated deleted the quest/m2/cache-routing-proof branch November 14, 2026 17:46
kixelated added a commit that referenced this pull request Nov 14, 2026
#8732 rewrote the warm-route ranking and the cache sweep on the path router:
the draft takes its Warm Routes text and the Route Offer refusal list keeps
this branch's, the sweep originates without a seqno, and the verify-route
skill points at its sweep. switch.md stays deleted.

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