Skip to content

feat(net): flights: a reader at a shed frame replays through a separate paced copy - #7944

Merged
kixelated merged 14 commits into
mainfrom
quest/m1/unbounded-groups/flights
Nov 5, 2026
Merged

kixelated merged 14 commits into
mainfrom
quest/m1/unbounded-groups/flights

Conversation

@kixelated

@kixelated kixelated commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m1/unbounded-groups/flights.md and deletes it; its remaining batch-fill work moves to the new Paced batch fills quest under the same epic.

Problem

A reader that reaches a frame its group copy shed gets Error::Evicted. That covers a late joiner wanting frame 0 of a long group, or a reader that fell more than the 32 MB tail behind. On a track that opts into refresh it asks for a new group; otherwise it has nothing to go on. Without flights, a long-lived group can't be replayed even when the publisher can serve old frames.

What changed

A flight is a separate copy of a group from the frame a reader needs. It is asked of the track's track::Requests handler, held to its own 32 MB tail, and paced by its fastest rider through the existing fastest-reader pacing.

  • Boarding (model/group.rs, poll_flown). A frame read that fails because the copy shed the frame read next boards a flight, provided the track has a handler. The reader rides an existing copy that admits it, or asks a new one from its frame. It then reads on from that copy at the same index, the way Recover adopts a replacement. Boarding happens below Recover, at the copy the cursor reads, so a relay's route copy asks its own upstream.

  • Admission (group::Handle::admits, group::ADMIT). A copy admits a reader whose frame it holds no further than ADMIT behind the end of the last frame its fastest reader took. ADMIT is 16 MiB (pace::AHEAD) less one full log segment (the new kio::log::Writer::SEGMENT_HEAP). Bytes are measured on the copy's own pace scale.

  • Handover (track::hand_over, TrackState::ride). A rider checks, whenever its prefetched batch is drained, for a copy further along (the cached copy or another flight) that admits its frame. If one does, it switches there at the same frame. Its flight is then unread, so the lite FETCH handler cancels it upstream, and every hop above follows.

  • Flight fills (model/track/fill.rs, FillState::fly). A flight is a fill of one group from a frame that never joins by sequence (covers). Readers asking from the same frame before it is answered share it. Its accepted copy is detached (TrackState::insert_flight): never served in place of the cached copy, registered weakly in TrackState::flights for admission, and shed under pool debt with the slot it replays. Fill::wanted never answers a flight from the cache, since the cached copy is the one that shed the frame.

  • Compressed groups (decision 1). A compressed group's flight starts at frame 0, since every later frame builds on the ones before it, and its rider reads and drops the frames before its own (Ride::skip), so pacing and the flight's own decoder see them in order. A compressed flight keeps its own content rather than sharing the cached copy's, whose inflated copy is further on. A plain reader of a compressed copy also boards once the shared inflated copy shed its frame, which the decoder, already past it, cannot inflate again. Admission additionally requires that a plain reader can still inflate its frame from the copy (every slice since the last inflated frame held, from frame 0).

  • Falling off. A rider that falls off its flight's tail boards again by the same rules. Boarding happens at most once per read, so a copy shed before the reader takes a frame (pool debt outrunning it) fails as Evicted rather than looping.

  • Fallback. With no handler, or a refused flight, the read stays Evicted and a reader of the newest group asks for a new group on a refresh track, as before.

  • Budgets and memory. A read waiting on its flight is judged by the reader's drift budget like one parked at the edge. Pool debt sheds every flight after the fresh slots, whether or not its group's cached copy survives.

  • moq-transport FETCH streams after FETCH_OK (decision 4, ietf/publisher.rs). PrepareFetch no longer reads the answer into memory (ReadFetch and FetchedGroup are gone). It finds the group (FindFetch), then learns its edge (poll_settle):

    • a finished group answers with the end it knows from metadata;
    • a live group answers only a range bounded at or below its edge;
    • a FETCH of the track's newest live group that reaches past its edge, including a whole-group one, is refused NOT_SUPPORTED ("FETCH of a live group must end below its live edge") before reading anything;
    • an older group still arriving (such as a relay's fill) and a joining FETCH's prefix are waited on until the group finishes or its edge passes the range.

    FETCH_OK's End Location, end_of_track, and properties come from that metadata, then the objects stream through the existing FetchGroupWrite (the fill path's writer), riding flights where the copy shed them, with no whole-group buffer and no GroupTooLarge cap. A failure after FETCH_OK, such as an object whose properties the draft cannot carry, resets the stream. FETCH stays single-group. The js/net IETF publisher already streams without copying the group (hooks.groupEnd waits on headers through the reader's normal tail), so it is unchanged.

  • Docs: the relay cache section (doc/bin/relay/config.md), the group module docs and MAX_CACHE_BYTES, track::Requests, and a handler note in rs/moq-net/MIGRATE.md.

  • Quests: flights.md is deleted (finished), and Paced batch fills takes its place in the epic's Required. The epic README records the four decisions below. Byte holes, Resume redesign, and moq-transport fill refill needed only the mechanism landed here, so their links to Flights point at this PR.

Tests

Model (model/track.rs), each against a 48 MiB live group, half again the tail:

  • a_late_joiner_rides_a_flight_and_hands_over: the joiner boards a flight from frame 0, reads it as the handler writes it, and hands over to the live copy between frames 1900 and 2000, as the 16 MiB bound predicts. The flight is then unwanted. Right after the handover the live writer fills its whole ahead window at once, and the rider still keeps its frame.
  • a_compressed_flight_starts_at_frame_0_and_its_rider_skips_ahead (new): a plain reader of a compressed group that fell off at frame 10 boards a flight asked from frame 0, gets nothing while the handler writes frames 0 to 9, reads on from frame 10, and hands over to the live copy.
  • joiners_within_reach_share_one_flight: two joiners at frame 0 share one fill. A third joining 8 MiB behind the rider boards the same flight; one joining 17 MiB behind asks its own.
  • a_slow_rider_falls_off_and_flies_on_its_own: the flight's writer is never held back by a rider that stops reading, and that rider boards a new flight from its frame once it falls off.
  • pool_debt_sheds_a_flight: under a 4 MiB pool, a live group and its flight stay under twice the pool.
  • a_refused_flight_falls_back_to_refresh: a refusal leaves Evicted, and a refresh track asks for a new group.
  • a_reader_boarding_a_flight_goes_stale: a reader whose flight the handler never answers still gets Old once a newer group supersedes it.
  • pool_debt_sheds_a_flight_without_its_cached_group: debt sheds a flight whose group's cached copy was evicted whole.

moq-transport publisher (ietf/publisher.rs, every FETCH draft):

  • a_standalone_fetch_of_the_live_group_ends_below_its_edge (replaces a_standalone_fetch_waits_for_the_largest_object_group): a whole-group FETCH of the live group and one ending past its edge are refused NOT_SUPPORTED; one ending at its edge is answered at once, without end_of_track.
  • a_standalone_fetch_waits_for_an_older_group_to_finish (new): an unfinished group older than the newest is waited on, then answered.
  • a_fetch_past_max_age_is_refused_not_truncated is removed: nothing is read before FETCH_OK any more, so there is no prefix to truncate, and a failure after it resets the stream.

Integration (tests/integration/flights.rs), publisher, two relays, and a viewer, with the publisher's handler serving fills from an archive:

  • a_late_joiner_replays_through_a_relay_chain (lite-05, lite-06, lite-07-wip): a late joiner at the bottom replays all 3000 frames from frame 0. Each hop serves the flight from its own flight, and lite-05 widens to frame 0. The joiner hands over, and the origin's single flight stops. No hop's pool held more than 80 MiB.
  • moq_transport_refuses_a_replay_of_a_live_group: over moq-transport-22 the upper relay refuses the lower one's whole-group FETCH of the live group before reading it, so the joiner gets an error and no flight reaches the origin.
  • moq_transport_relays_a_cold_finished_group (new): a relay holding none of a finished 48 MiB group answers a viewer's moq-transport FETCH once its own copy finishes arriving, then streams it. Times out without the parked cursor described under Choices.
  • moq_transport_replays_a_finished_group (new): the same chain with the group finished. Each moq-transport hop answers FETCH_OK from metadata and streams the group through its own flight from frame 0, the joiner reads all 3000 frames and the group's end, and the origin's one flight ends.
  • session_idle::a_fetch_keeps_both_ends_busy now covers lite only: moq-transport refuses its whole-group FETCH of the live group.

Measurements

group_flights (rs/moq-net/benches/group.rs): a 48 MiB live group replayed from frame 0 to late readers, each joining once the one before it read apart. Swept over readers and spacing; this host (desktop, 24 cores), quest gate, criterion medians:

readers apart flights frames fetched peak pool time per frame read
1 any 1 3,066 49 MiB 3.0-3.4 ms ~1.0 µs
4 0 1 3,066 49 MiB 3.8 ms 0.31 µs
4 8 MiB 2 6,138 49 MiB 7.0 ms 0.57 µs
4 24 MiB 4 12,264 49 MiB 12.7 ms 1.0 µs
16 0 1 3,066 49 MiB 5.5 ms 0.11 µs
16 8 MiB 10 22,498 50 MiB 29.2 ms 0.59 µs
16 24 MiB 16 49,056 49 MiB 50.7 ms 1.0 µs
  • Joiners share a flight only within 16 MiB of its fastest rider, so 8 MiB spacing pairs them up.
  • A flight runs up to 16 MiB ahead of its rider, so it fetches about one window past the handover point (3,066 of 3,072 frames here). That is the plan's "a merge costs at most 16 MB of duplicate transfer".
  • Flights share the group's canonical content, so in one process the pool held about one tail plus one flight however many flew. The cost of more flights is upstream transfer.

The ordinary read path does not regress. just bench-fanout origin/main, which counts instructions per delivered group independent of host load, gave these results against the merge base 9227f82bc1:

scenario subscribers base this PR change
model 1 23,230.8 23,287.0 +0.24%
model 64 9,808.0 9,808.1 0.00%
model 512 9,601.8 9,602.0 0.00%
session 1 218,495.2 218,872.5 +0.17%
session 64 108,760.8 109,101.5 +0.31%
session 512 107,070.5 107,403.4 +0.31%

Every cell is within the ~0.5% that hash seeds alone move a count (bench/fanout.py).

These figures are from head 20b75b38de, before this round. Since then the plain read path gained one branch per read (the skip count, checked only while a reader carries a ride) and one per shed check, both off the common path.

Decisions

Settled by the maintainer on 2026-11-05 (/quest-plan), recorded in the epic README's Plan:

  1. Compressed flights:
    • ✅ (a) a compressed flight starts at frame 0 and its riders skip to their frame, costing the prefix's transfer once per flight
    • (b) carry the inflater's state from the cached copy, which needs a snapshot per segment
    • (c) compressed groups keep refresh or Evicted
  2. Paced batch fills (the moq-dev/moq#7339 follow-up):
  3. Cap on concurrent flights per group:
    • ✅ (a) no cap; revisit with relay evidence
    • (b) cap per group, sending extra joiners to refresh or Evicted
  4. moq-transport replays:
    • (a) keep the one-tail bound and refuse replays
    • (b) stream FETCH answers after FETCH_OK so they ride flights
    • ✅ neither as listed: answer FETCH_OK from metadata, refusing any FETCH of a live group unless bounded below the live edge, then stream the objects; a failure after FETCH_OK resets the stream; FETCH stays single-group. The maintainer: "I want to make sure we don't have to buffer everything in memory before serving a FETCH, but we do need to buffer to at least learn the live edge [...] please don't make it complicated."

Choices made without asking

  • Shedding unit. The plan's "less one shedding unit (a log segment)" is kio::log::Writer::<Entry>::SEGMENT_HEAP, the heap of the largest (64-slot) segment.
  • Boarding at the copy, below Recover, so each relay hop asks its own upstream and the recursion falls out of the existing FETCH paths.
  • Handover checks run only for a rider, and only when its prefetched batch is drained, so a reader not on a flight pays nothing.
  • Flight priority. A flight asks with priority 0, the publisher's max age, and ascending order. A group reader doesn't know its subscription's preferences.
  • Boarding retries once per read. A copy that sheds the frame before the reader takes it, within the same read, fails Evicted. A later read may board again.
  • Handler contract unchanged. Fill::is_wanted already says that once a group is accepted and taken, group::Producer::demand says who reads it, so a handler stops a flight on both. The lite FETCH handler already does this.
  • Compressed riders skip by reading. A rider reads and drops the prefix rather than waiting at its frame, since the flight's inflated copy inflates in order and would otherwise shed slices before inflating them. A raw (compressed egress) rider skips the same way and continues on the flight's slices, relying, as Recover already does, on a group's stored slices being the same in every copy.
  • What "live" means for a FETCH. The live group is the newest group of a track fed live (track::Consumer::is_live). An unfinished older group, or any group on a fetch-only copy, is treated as still arriving and waited on until it finishes or its edge passes the range, which is the buffering needed to learn the edge; the cache holds it, the FETCH holds nothing. A refused live FETCH answers NOT_SUPPORTED. The moq-transport subscriber still asks a flight's group whole, so moq-transport hops replay finished groups only, as the maintainer's rule implies.
  • A finished group's End Location comes from its last object, or its END_OF_GROUP status object's ID when it has one, since FETCH objects carry no status.
  • A settling FETCH parks a cursor past every frame (with_range(u64::MAX..)), so a cold group's paced ingest runs to its end instead of waiting on a reader that takes nothing. A group past one tail then sheds its front, and the stream after FETCH_OK rides a flight for it, so a cold relay fetches such a group's front twice. Resolving the answer from upstream metadata instead would need a per-draft path.
  • A rider that boards again while skipping keeps the frames it still has to skip.
  • A fetch stream stops with Unsupported once its group turns unfetchable mid-way, which the old read checked per object before FETCH_OK.
  • Consumer's cold state is merged into one box (Carry: Recover and the ride) to stay within its 184-byte cap.

Impact

  • Public API: additive only. kio::log::Writer::SEGMENT_HEAP (new const). Behavior: a track with a track::Requests handler now gets single-group fills from a frame for groups its cache holds live, from frame 0 on a compressed track (see MIGRATE.md).
  • Wire: no new messages or fields. Flights use the existing frame-offset FETCH (lite-06+) and widen to frame 0 on older lite. A moq-transport publisher now refuses a FETCH of a track's newest live group that reaches past the group's edge (it used to wait for the group to end), answers FETCH_OK before reading the group, and resets the fetch stream on a failure after it.

Checks

  • cargo nextest run -p moq-net --all-features --all-targets: 3479 passed, 13 skipped, on the final source (729e30dbec).
  • cargo clippy -p moq-net -p kio --all-targets --all-features -- -D warnings: clean on the final source. cargo check -p moq-net --no-default-features: clean.
  • quest check --base origin/main: 588 documents ok.
  • just check on da54613d88: passed, 11521 tests, 137 skipped. Since then only MIGRATE.md wording and the two review fixes in moq-net changed, covered by the scoped runs above.

Review

Codex (quest review), gpt-6.1-sol, medium effort, a new session after the design change (see the Design change: comment). Latest round: on head 729e30dbec95057ef40f816659ecaeb20852d102, no findings.

  • Round 1 (head b6a62ffe46), two findings, both fixed:
    • High: a relay FETCH of a cold finished group larger than its ingest window never answered, since nothing read the copy while the FETCH waited for its end, so paced ingest stalled. Fixed with the parked cursor; reproduced by moq_transport_relays_a_cold_finished_group.
    • Medium: a compressed rider that boarded again while skipping to its frame reset its skip and could return a prefix frame. Fixed: both boarding branches keep the outstanding skip.
  • Round 2 (head 729e30dbec): no findings.

Earlier rounds, before the design change: round 1 on d6765254ee (three findings, fixed) and round 2 on 20b75b38de (none).

Follow-ups

None.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 8 commits November 4, 2026 23:16
…copy and hands over

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opy ahead's burst

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…answers stay within one tail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and the quest's remaining work

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… flight, FETCH answers count each object

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Flights feat(net): flights: a reader at a shed frame replays through a separate paced copy Nov 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 4 commits November 5, 2026 08:48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ms after FETCH_OK

Applies the maintainer's 2026-11-05 decisions on the flights PR:

- A compressed group's flight starts at frame 0 and its riders skip to their
  frame, inflating the prefix with the flight's own decoder. A plain reader
  also boards once its inflated copy shed the frame it reads next.
- A moq-transport FETCH learns its group's edge and answers FETCH_OK from
  metadata, then streams the objects, riding flights, with no whole-group
  buffer. A FETCH of the track's newest live group is refused unless it ends
  at or below the group's edge; an older group still arriving is waited on.
- Paced batch fills move to a quest of their own; flights.md is finished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…once its group turns unfetchable

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

Copy link
Copy Markdown
Collaborator Author

Design change: applying the maintainer's 2026-11-05 decisions. Compressed groups now fly, from frame 0, with riders skipping to their frame. The moq-transport FETCH path no longer reads the answer into memory before FETCH_OK: it learns the group's edge, answers from metadata, refuses a FETCH of the newest live group that reaches past its edge, and streams the objects after FETCH_OK through the fill path's FetchGroupWrite, riding flights. Paced batch fills move to their own quest. The local review restarts with quest review --new.

(Written by Claude Opus 5.5)

…ing rider keeps its skip

A FETCH waiting for a cold group to finish arriving parks a cursor past every
frame, so the copy's paced ingest runs on instead of waiting on a reader that
takes nothing. A compressed rider that boards again while skipping to its
frame carries the frames it still has to skip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 5, 2026 17:45
@coderabbitai

coderabbitai Bot commented Nov 5, 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 16 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: 169dd181-2313-47bc-905c-5b2f3415e505
Commits

Reviewing files that changed from the base of the PR and between d0c73e3 and 729e30d.

Files selected for processing: 18

  • doc/bin/relay/config.md
  • quest/m1/unbounded-groups/README.md
  • quest/m1/unbounded-groups/byte-holes.md
  • quest/m1/unbounded-groups/flights.md
  • quest/m1/unbounded-groups/paced-batch-fills.md
  • quest/m2/ietf-fill-refill.md
  • quest/m2/resume-redesign.md
  • rs/kio/src/log.rs
  • rs/moq-net/MIGRATE.md
  • rs/moq-net/benches/group.rs
  • rs/moq-net/src/ietf/publisher.rs
  • rs/moq-net/src/model/group.rs
  • rs/moq-net/src/model/pace.rs
  • rs/moq-net/src/model/track.rs
  • rs/moq-net/src/model/track/fill.rs
  • rs/moq-net/tests/integration/flights.rs
  • rs/moq-net/tests/integration/main.rs
  • rs/moq-net/tests/integration/session_idle.rs
  • 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

@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 729e30d. Detached paced copies and streaming after metadata-derived FETCH_OK are a reasonable direction without restoring whole-group buffering.

  1. [P2] Cancel the old flight when recovering onto another route (rs/moq-net/src/model/group.rs:4065–4075). poll_resumed adopts the replacement cursor but leaves carry.ride.boarding from the old route. This block polls that old request first: an unanswered flight keeps the reader pending despite available replacement data, a refusal yields spurious Evicted, and an old success can overwrite the replacement cursor. Cancel old boarding on adoption, preserving any compressed-prefix skip separately. Test an unanswered flight on A followed by a healthy route B.
  2. [P2] Propagate expiry failures while settling a FETCH (rs/moq-net/src/ietf/publisher.rs:220–224). The parked cursor's result is discarded while poll_settle waits on raw group closure. An older unfinished group that exceeds the reader's max-age budget returns Old from the cursor but can leave FETCH pending until unrelated FIN, eviction or cancellation. Propagate terminal cursor errors or explicitly apply expiry during settlement; test an older unfinished group aging out without FIN.

Verification: GitHub-only static review of recovery, pacing, expiry and test paths. I did not run tests.

@kixelated
kixelated added this pull request to the merge queue Nov 5, 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.

Re-raising the two findings from the prior automated review on this head, both verified against the current source. The flights/pacing design itself reads as sound: the admission window's arithmetic (ADMIT = pace::AHEAD - SEGMENT_HEAP, with held() as the log base less the adopted front) leaves the reader's frame in place when the writer runs a whole window ahead, and admits correctly rejects adopted front frames and uncompressed readers of a compressed copy whose inflated copy shed the frame. The issues below are in the state transitions around that design, not in its premise.

1. Route recovery leaves the reader's flight state behind

poll_resumed (rs/moq-net/src/model/group.rs:3742-3752) replaces self.cursor with the replacement copy's cursor but does not touch self.carry. The Carry.ride (:3409, Ride.boarding at :3432) therefore survives the adoption, and the next poll_flown (:4059-4076) polls the old route's boarding first:

  • an unanswered boarding returns Pending, parking the reader despite the replacement copy having its frame (:4075);
  • a refusal clears the ride and returns Evicted (:4069-4071), even though the replacement route is healthy;
  • a success calls adopt_copy(copy) and resets cursor.index = start (:4077-4082), moving the reader back onto the old route's flight and overwriting the replacement cursor's index and copy.

Concrete case: a reader boarding a flight on route A while a newer route B serves its frame. B's copy is adopted, then the stale A boarding wins and either parks or reverts the reader. Clear the ride when a replacement copy is adopted in poll_resumed, so no boarding or flight outlives the route it belongs to; keep the compressed-prefix skip only when the replacement read still needs it. A test with an unanswered flight on A followed by a healthy B would cover this.

2. A settling FETCH discards the parked cursor's expiry

rs/moq-net/src/ietf/publisher.rs:220-224:

let mut past: group::Consumer = found.1.clone().with_range(u64::MAX..);
let end = wait!(Settle => {
    let _ = past.poll_next_frame(waiter);
    poll_settle(waiter, track, range, &found.1, self.joined)
})?;

The parked cursor's result is dropped. A fetch_group consumer does carry the caller's max_delay as a FetchExpiry (rs/moq-net/src/model/track.rs:5761-5774), so past.poll_next_frame returns Err(Error::Old) once an older unfinished group falls past that budget. That error never reaches poll_settle, whose group.poll_closed only resolves on finish or abort (rs/moq-net/src/model/group.rs:2304-2306). The FETCH then stays pending until unrelated FIN, eviction, or cancellation — a hang where the previous read-before-FETCH_OK path refused the request. The removed a_fetch_past_max_age_is_refused_not_truncated covered exactly this; its replacement should fail the settle on a terminal cursor error (or apply the expiry during settlement), with a test for an older unfinished group aging out without FIN.

Verdict: request changes for the two above. The rest of the diff — the flight fill's covers, Fill::wanted bypassing the cache for flights, pool debt shedding flights after the fresh slots, the compressed flight starting at frame 0 with the rider's Ride::skip, and the moq-transport metadata-derived FETCH_OK — matches the settled decisions and the documented tests.

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

Merged via the queue into main with commit 8cd44b5 Nov 5, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m1/unbounded-groups/flights branch November 5, 2026 18:36
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