From 404a0c0c03475a64a02aa617252178a86c6c2c2b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 10:13:15 -0700 Subject: [PATCH 1/3] quest: plan the broadcast lifecycle epic from the linger audit Broadcasts lose their finished state in moq-net; retention becomes a fixed moq-mux policy; the TS export wait becomes --follow and stops guessing with a 1s grace; SRT tells a clean end from a failure; the unused pool linger prop goes. Aligns cli-flags, binding-trim, sink-restart-follow and ietf-finish. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 5 ++ quest/m1/api-audit/README.md | 4 +- quest/m1/api-audit/binding-trim.md | 2 - quest/m1/api-audit/cli-flags.md | 10 ++-- quest/m1/broadcast-lifecycle/README.md | 59 +++++++++++++++++++++++ quest/m1/broadcast-lifecycle/retention.md | 42 ++++++++++++++++ quest/m1/broadcast-lifecycle/srt-end.md | 22 +++++++++ quest/m1/broadcast-lifecycle/stateless.md | 44 +++++++++++++++++ quest/m1/broadcast-lifecycle/timers.md | 24 +++++++++ quest/m1/broadcast-lifecycle/ts-follow.md | 36 ++++++++++++++ quest/m1/sink-restart-follow.md | 4 +- quest/m2/ietf-finish.md | 3 +- 12 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 quest/m1/broadcast-lifecycle/README.md create mode 100644 quest/m1/broadcast-lifecycle/retention.md create mode 100644 quest/m1/broadcast-lifecycle/srt-end.md create mode 100644 quest/m1/broadcast-lifecycle/stateless.md create mode 100644 quest/m1/broadcast-lifecycle/timers.md create mode 100644 quest/m1/broadcast-lifecycle/ts-follow.md diff --git a/quest/m1/README.md b/quest/m1/README.md index d50859beb67..c680b1d55e4 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -68,6 +68,10 @@ announce churn on today's hop-list mesh, shipping with the next cut from route layer ships; and stats groups as tracks, so per-group stats stop costing cluster announces. +Added 2026-10-23 (maintainer, from a linger audit): broadcasts lose their +finished state and retention moves to moq-mux, a break in every binding, so +it lands before the cut. + ## Required - [Epoch correctness](/quest/m0/broadcast-epoch/README.md) - publisher identity, duplicate suppression, and catalog pinning gate the breaking cut @@ -97,6 +101,7 @@ tracks, so per-group stats stop costing cluster announces. - [Moxygen minimal interop](/quest/m1/smoke-moxygen.md) - waiting on Moxygen to send a registered filter type, then the smoke lane stops hiding its failure - [More tests under load](/quest/m1/test-flakes-2/README.md) - the second round of load-only failures, one quest per flake, fixed at the cause - [A public model](/quest/m1/public-model/README.md) - every model item a built-in session reaches is public, unnecessary, or marked deliberately private, so a custom publisher, subscriber, or relay builds from the public model alone +- [Broadcast lifecycle](/quest/m1/broadcast-lifecycle/README.md) - broadcasts are online or offline with no finished state in moq-net, finished-broadcast retention is a fixed moq-mux policy, and each linger timer has one meaning - [A smaller, safer API than release](/quest/m1/api-audit/README.md) - every surface added since release has a consumer or is gone, overlapping operations are collapsed, names mirror across Rust, JS, and the bindings, and every break has its MIGRATE entry - [JavaScript FETCH](/quest/m1/js-fetch.md) - browser publishers answer IETF FETCH through the JS ranges request surface - [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - native FILL_TIMEOUT bounds upstream waiting on drafts 18-22, alongside local content-age enforcement diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 250bb6bc3fa..a53bd5c3445 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -63,8 +63,8 @@ Rejected in planning: `close` collides with `AutoCloseable`, and no single verb fits every language's reserved names. - A follow policy enum folding `linger` and `stitch`: one name per meaning - ([CLI flags](/quest/m1/api-audit/cli-flags.md)) fixes the confusion more - cheaply. + ([TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md) + renames the export wait `--follow`) fixes the confusion more cheaply. ## Required diff --git a/quest/m1/api-audit/binding-trim.md b/quest/m1/api-audit/binding-trim.md index e7991fd211e..b282f1e747f 100644 --- a/quest/m1/api-audit/binding-trim.md +++ b/quest/m1/api-audit/binding-trim.md @@ -23,8 +23,6 @@ Decided 2026-10-22 (API audit): wrapper pass-throughs uses them. - `MoqBroadcastConsumer::resolve(reference, epoch)` takes two adjacent `Option`s; take `MoqRendition`, as the rendition consumers do. -- Document that a dropped `MoqBroadcastFinish` closes the broadcast, which a - garbage-collected language (Kotlin, Go, Dart) does late. - moq-c: `moq_bandwidth_set_target` has no consumer beyond the README; keep it only if [native rung disable](/quest/m1/simulcast-rung-disable.md) uses it. Align `moq_client_defaults` wrapping across languages. diff --git a/quest/m1/api-audit/cli-flags.md b/quest/m1/api-audit/cli-flags.md index 2d18e0be3f9..88fccdfa8cd 100644 --- a/quest/m1/api-audit/cli-flags.md +++ b/quest/m1/api-audit/cli-flags.md @@ -17,11 +17,11 @@ Decided 2026-10-22 (API audit): raw TS multiplex on `import|export ts`. Rename the fmp4/hls one to `--container cmaf` or another distinct name, consistent with [container formats](/quest/m1/api-audit/container-format.md). -- `linger`: publishers already linger on a clean end in rtmp, srt, rtc, and - the CLI. Drop moqsink's new boolean `linger` property and ``'s - `linger` attribute so every publisher lingers; NULL and `close` still end - at once. `linger` then means only the subscriber-side wait for a - replacement (`moq export --linger`). +- `linger`: drop moqsink's new boolean `linger` property and + ``'s `linger` attribute, keeping today's close at the end. + Revised 2026-10-23 (maintainer): don't make every publisher finish through + moq-net's `Finish`; [broadcast lifecycle](/quest/m1/broadcast-lifecycle/README.md) + deletes it and owns completion and retention. - gst: moqsrc's `versions` (comma list) versus the CLI's repeatable `--connect-version`; mirror one name, give moqsink the same, and give both elements the same connection properties (moqsrc lacks diff --git a/quest/m1/broadcast-lifecycle/README.md b/quest/m1/broadcast-lifecycle/README.md new file mode 100644 index 00000000000..909b355a5d4 --- /dev/null +++ b/quest/m1/broadcast-lifecycle/README.md @@ -0,0 +1,59 @@ +# Broadcasts are online or offline, and retention is app policy + +## Goal + +A moq-net broadcast has no lifecycle state of its own: it is online while +announced and offline once closed. The app marks the end of its content with +a track FIN (hang: finish the catalog). Keeping a finished broadcast up for +late readers is a fixed-duration app policy in moq-mux, not a deadline moq-net +derives from track max-age. Every other timer called "linger" has one meaning +per name. + +## Plan + +Decided with the maintainer on 2026-10-23, from an audit of every linger +timer on main at 1582fdc07d: + +- `broadcast::Producer::finish` exists only in moq-net. Nothing reaches the + wire: ANNOUNCE active/ended already says online/offline, and a track FIN + already says a track's content ended. A BROADCAST wire message was + considered and rejected; it adds no behavior over those two and would need a + moq-transport mapping. +- The finished linger was inconsistent and partly accidental: + - Its duration is the max of track max-ages, which describe how long a + superseded group stays fresh and never the newest one. The JS publish + catalog declares `Number.MAX_SAFE_INTEGER` max-age, so every browser + publisher lingered the full 30s; Rust catalogs declare none. + - It counted from different events: Rust from the first poll of `wait`, + JS from the `wait` call, UniFFI from `finish`, and C from the caller's + own clock. + - Only Rust ended it when a newer epoch appeared at the path. +- Closing a broadcast already leaves tracks handed out playing to their end, + and session drain delivers their tail to attached subscribers. Retention + only serves readers that arrive after the end. +- A retaining publisher ignores a newer instance at its path: if the newer + one leaves, the old one is announced again, and readers see its finished + catalog and stop. Simpler than watching for supersession, and acceptable + under online/offline. +- The other timers stay. Each is a cache or debounce with its own name and + owner: the Rust idle track copy (30s), discovery (2s), the JS connection + pool (2s), and the relay's `[stats] linger` (5m). They are not cross-language + lifecycle guarantees. +- After this epic, user-facing "linger" means only publisher retention + (`moq` publishing subcommands, moqsink) and the relay's stats setting. The + export-side wait becomes `--follow`. +- Docs change inline in each quest; no separate lifecycle page. + +## Required + +- [Publisher retention in moq-mux](/quest/m1/broadcast-lifecycle/retention.md) - moq-cli, the gateways, and moqsink keep a finished broadcast up for a fixed `--linger` (default 30s) through a moq-mux helper instead of moq-net's `Finish` +- [Stateless broadcasts](/quest/m1/broadcast-lifecycle/stateless.md) - `Finish`, its linger, and finished-only supersession are gone from moq-net, `@moq/net`, moq-ffi, moq-c, and every binding +- [TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md) - `export ts|srt --linger` becomes `--follow `, a replacement's catalog has no separate deadline, and a track failure is classified by its cause instead of a 1s grace +- [SRT tells a clean end from a failure](/quest/m1/broadcast-lifecycle/srt-end.md) - an SRT ingest whose peer goes silent aborts instead of finishing as if it had shut down +- [Timer cleanups](/quest/m1/broadcast-lifecycle/timers.md) - `ConnectionProps.linger` is gone and stale linger references are fixed + +## Related + +- [moq-transport finish interop](/quest/m2/ietf-finish.md) - the interop `finish` case, whose late readers rely on retention +- [CLI and gst flags](/quest/m1/api-audit/cli-flags.md) - removes the opt-in `linger` booleans, leaving completion to this epic +- [Publication owner](/quest/m1/api-audit/announce-owner.md) - adds `origin::Publication`; it must not forward a `finish` this epic deletes diff --git a/quest/m1/broadcast-lifecycle/retention.md b/quest/m1/broadcast-lifecycle/retention.md new file mode 100644 index 00000000000..b8ff2070bbe --- /dev/null +++ b/quest/m1/broadcast-lifecycle/retention.md @@ -0,0 +1,42 @@ +# [M] Publisher retention in moq-mux + +## Goal + +moq-cli imports (stdin, HLS), the RTMP, SRT, RTC, and HLS gateways, and +moqsink keep a cleanly ended broadcast announced and serving its finished +tracks for a fixed duration counted from the end, then close it. They no +longer use moq-net's `broadcast::Finish`. The duration is `--linger +` on moq-cli's publishing subcommands and a `linger` duration +property on moqsink, defaulting to 30s; 0 closes at once. An interrupt +(Ctrl-C, moqsink NULL) ends it at once. + +## Plan + +Decided with the maintainer on 2026-10-23: + +- One small moq-mux helper owns it, shared by moq-cli, `moq-rtmp`, + `moq-srt`, `moq-rtc`, and moqsink. It finishes the catalog and media + tracks, holds the broadcast and track producers, and closes the broadcast + when the deadline passes. moq-net gains nothing. +- The deadline is fixed when the content ends and does not depend on track + max-age or on when anything is polled. 30s matches today's effective + behavior (media max-age) and the relay's idle-copy window. +- It ignores a newer instance announced at the path; see the epic. +- `` and the FFI/C bindings do not use it: they finish their + catalog and close, and a binding app owns its own process lifetime. +- moq-cli RTSP closes at once today (`rs/moq-cli/src/rtsp.rs`); give it the + helper too, or say why not. +- Keep each gateway's existing split between a clean end and a failure + (RTMP unpublish vs disconnect, WHIP DELETE vs a vanished peer); only a + clean end retains. SRT's split is + [its own quest](/quest/m1/broadcast-lifecycle/srt-end.md). +- Test the helper with mocked time: a late subscriber gets the whole tail + before the deadline, and the path is unroutable after it. +- Docs: `doc/bin/cli.md`, `doc/bin/gstreamer.md`, the gateway pages under + `doc/bin/`. + +Public API: moq-mux gains the helper. moq-cli gains `--linger` on publishing +subcommands; `moq export --linger` keeps its meaning until +[TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md) +renames it. moqsink's `linger` returns as a duration (the boolean never +shipped in a release, so no MIGRATE entry). Wire: none. diff --git a/quest/m1/broadcast-lifecycle/srt-end.md b/quest/m1/broadcast-lifecycle/srt-end.md new file mode 100644 index 00000000000..8a215e315ac --- /dev/null +++ b/quest/m1/broadcast-lifecycle/srt-end.md @@ -0,0 +1,22 @@ +# [S] SRT tells a clean end from a failure + +## Goal + +An SRT ingest whose peer stops sending without a shutdown handshake ends its +broadcast with an error, while a peer that shuts down cleanly finishes it. +Today both finish (`rs/moq-srt/src/server.rs`, the `Ok(())` arm after the +socket stream ends), because srt-tokio ends the stream the same way for both. + +## Plan + +Decided with the maintainer on 2026-10-23: fix it at the source. + +- Find how srt-tokio reports a shutdown handshake versus a peer timeout. If + it exposes the difference, use it in both the server and the `Client` + pull (`serve_publish`). +- If it does not, an upstream srt-tokio change is needed: it is outside the + moq-dev and kixelated orgs, so ask before posting there. +- Test both ends with a mocked clock: a silent peer aborts, a clean shutdown + finishes. + +Public API: none. Wire: none. diff --git a/quest/m1/broadcast-lifecycle/stateless.md b/quest/m1/broadcast-lifecycle/stateless.md new file mode 100644 index 00000000000..7f0b1354d72 --- /dev/null +++ b/quest/m1/broadcast-lifecycle/stateless.md @@ -0,0 +1,44 @@ +# [L] Stateless broadcasts + +## Goal + +moq-net and `@moq/net` broadcasts have no finished state. `Producer::finish`, +`Finish`, `Finish::linger`, `Finish::wait`, and the origin's finished-only +supersession (`Consumer::supersede`, `routes.superseded`, `Announcer::superseded`, +`BroadcastState.finished`/`superseded`) are deleted, along with moq-ffi's +`MoqBroadcastFinish` and moq-c's `moq_publish_finish` and +`moq_publish_finish_close`, and their wrappers in every binding. A publisher +ends its content by finishing its tracks (hang: the catalog) and goes offline +by closing the broadcast. + +## Plan + +Decided with the maintainer on 2026-10-23; see the epic for why. + +- Keep the epoch routing that lets a restarted publisher win + (#6452); only supersession of a finished broadcast goes. +- Prove the property this relies on before deleting: a relay keeps + delivering a finished track to its existing downstream subscribers after + the upstream publisher's session closes, on lite and moq-transport. Fix + it at the source if it does not. +- The interop `finish` case must keep passing on lite. Each interop + publisher (Rust through the moq-mux helper; JS, C, C++ and the bindings by + holding their finished broadcast open) keeps its broadcast up long enough + for the late readers. +- `js/publish` drops its `Finish` bookkeeping + (`js/publish/src/broadcast.ts`, the catalog's `Number.MAX_SAFE_INTEGER` + max-age only if nothing else needs it). `js/net` drops `hooks.linger` and + its `CACHE_WINDOW` use there. +- Update `doc/concept` with one paragraph on the model (online = announced, + end of content = track FIN, retention = app policy), plus `doc/lib/*` and + every affected package's `MIGRATE.md`. +- Coordinate with [binding trim](/quest/m1/api-audit/binding-trim.md) and + [publication owner](/quest/m1/api-audit/announce-owner.md), which touch the + same handles. + +Public API: breaking in moq-net, `@moq/net`, moq-ffi, moq-c, and every +binding. Wire: none. + +## Required + +- [Publisher retention in moq-mux](/quest/m1/broadcast-lifecycle/retention.md) - Rust publishers stop using `Finish` first, so none loses its tail in between diff --git a/quest/m1/broadcast-lifecycle/timers.md b/quest/m1/broadcast-lifecycle/timers.md new file mode 100644 index 00000000000..5319a24cf6d --- /dev/null +++ b/quest/m1/broadcast-lifecycle/timers.md @@ -0,0 +1,24 @@ +# [XS] Timer cleanups + +## Goal + +`@moq/net`'s `ConnectionProps.linger` is deleted and the pool always uses its +fixed 2s idle timer. Comments that cite timers which no longer exist are +fixed. + +## Plan + +Decided with the maintainer on 2026-10-23: + +- `ConnectionProps.linger` (`js/net/src/connection/pool.ts`) has no caller + outside the pool, and the pool keeps the largest value any handle ever + asked for, even after that handle leaves. Delete the prop instead of + defining a merge rule. +- `rs/moq-video/src/decode/consumer.rs` cites `TRACK_IDLE_LINGER`, which is + now `moq_net::model::track::IDLE_LINGER` (crate-private). +- The OBS "Give up after" help text (`cpp/obs/src/moq-settings.cpp`) says the + broadcast "lingers for viewers across the gap", but the setting is a + reconnect backoff timeout. Say what it does. + +Public API: `@moq/net` breaking (`ConnectionProps.linger` removed). Wire: +none. diff --git a/quest/m1/broadcast-lifecycle/ts-follow.md b/quest/m1/broadcast-lifecycle/ts-follow.md new file mode 100644 index 00000000000..b02bb953795 --- /dev/null +++ b/quest/m1/broadcast-lifecycle/ts-follow.md @@ -0,0 +1,36 @@ +# [M] TS export follows by cause + +## Goal + +`moq export ts --linger` and `moq export srt --linger` become `--follow +`: how long to wait for the publisher, or with `--stitch` a +replacement, to come back. The default 0 means don't follow. The Rust +`container::ts::Follower::with_linger` and moq-srt's `Subscribe::with_linger` +and `Client::with_linger` become `with_follow`. A replacement's first catalog +is awaited until it arrives or the replacement withdraws, with no deadline of +its own. A track failure is classified by its cause, not by waiting +`CLOSE_GRACE` (1s) to see whether the announcement also goes. + +## Plan + +Decided with the maintainer on 2026-10-23: + +- One knob, one meaning. Today `--linger` also bounds the stitch catalog + fetch (`rs/moq-mux/src/container/ts/follower.rs`, `Awaiting { deadline }`), + so `--stitch --linger 0` fails on a cold remote replacement. The + replacement's own lifetime is the bound instead. +- `CLOSE_GRACE` exists because the track error and the announcement removal + arrive on different streams. Find what tells "the publisher left" from "this + export failed" in the track's own end: the abort cause a relay gives a copy + whose upstream went away. If no cause distinguishes them today, add one at + the source. A wire error code change updates the drafts. +- The hidden `--linger` on the other container exports + (`rs/moq-cli/src/args.rs`) keeps refusing; point its message at `--follow` + only where following applies. +- Coordinate the name with [readers follow restarts](/quest/m1/sink-restart-follow.md), + which owns `moq_mux::follow::Instances`. +- Docs: `doc/bin/cli.md`, `rs/moq-cli/MIGRATE.md`, `rs/moq-mux` and + `rs/moq-srt` MIGRATE entries. + +Public API: moq-mux and moq-srt renames (breaking); moq-cli flag rename. +Wire: none unless the failure cause needs a new code. diff --git a/quest/m1/sink-restart-follow.md b/quest/m1/sink-restart-follow.md index b76f09ecbfe..e8d5fe10d61 100644 --- a/quest/m1/sink-restart-follow.md +++ b/quest/m1/sink-restart-follow.md @@ -43,7 +43,9 @@ Audit and fix the rest the same way: (`EXT-X-DISCONTINUITY`); a failed timeline answers its playlists with a no-store 5xx. - `export rtmp`, `export srt`, `export rtc`, both `--listen` (per-viewer streams in `moq-rtmp`, `moq-srt`, `moq-rtc`) and `--connect` (one push). - SRT already rides the TS follower (`--linger`, `--stitch`); confirm its + SRT already rides the TS follower (`--linger`, becoming `--follow` in + [TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md), + and `--stitch`); confirm its listen side does too. FLV and WebRTC need a decision. - `transcode`: its source broadcast; following means restarting the ladder on the new instance. diff --git a/quest/m2/ietf-finish.md b/quest/m2/ietf-finish.md index 8a3eb7c7d3f..a3fbfc9f4ab 100644 --- a/quest/m2/ietf-finish.md +++ b/quest/m2/ietf-finish.md @@ -3,7 +3,8 @@ ## Goal The interop `finish` case passes on every even moq-transport draft, not only -moq-lite: each reader that joins a finished broadcast during its linger gets +moq-lite: each reader that joins a finished broadcast while its publisher retains it +([broadcast lifecycle](/quest/m1/broadcast-lifecycle/README.md)) gets the whole ten-frame tail, natural media and catalog EOF, the exact broadcast's withdrawal, and the publisher's later health broadcast through the same session. Until then `interop.sh` records those cells as skipped; delete that From 4ad6c62884a40b9c456191f0a7fe53bb1a5b4ac7 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 11:15:31 -0700 Subject: [PATCH 2/3] quest: migrate names the final finish API, not the linger handle Co-Authored-By: Claude Opus 5.5 --- quest/m1/api-audit/migrate.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/quest/m1/api-audit/migrate.md b/quest/m1/api-audit/migrate.md index e35e053117b..dd33ba38bf9 100644 --- a/quest/m1/api-audit/migrate.md +++ b/quest/m1/api-audit/migrate.md @@ -18,8 +18,11 @@ Decided 2026-10-22 (API audit). Known gaps, verify each against (`Shutdown(ctx) error`, and `Client.Close()` now blocks while draining), `kt/moq`, `dart/moq` (`Moq.close()` returns `Future`). - Bindings: merge the contradictory `finish` entries ("removed (#4152)" and - "the new `finish` lets go (#6157)") into one: `finish()` now returns a - linger handle; call `close()` for the old behavior. + "the new `finish` lets go (#6157)") into one that names the final API: + once [stateless broadcasts](/quest/m1/broadcast-lifecycle/stateless.md) + lands, the broadcast `finish()` is gone, so a publisher finishes its tracks + and closes the broadcast. If this lands first, leave the entry for that + quest to rewrite rather than document the linger handle. - Bindings: `rs/moq-ffi/MIGRATE.md` says a raw track is timed by default, but `MoqTrackInfo.timescale` defaults to `None` (untimed) while release defaulted to microseconds; the net change is timed to untimed by default. From ea366084fcd19f65843343f9557446b89499696a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 16:37:06 -0700 Subject: [PATCH 3/3] Drop link to the deleted public-model quest Co-Authored-By: Claude Sonnet 5.5 --- quest/m1/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/quest/m1/README.md b/quest/m1/README.md index c838de6825d..d01b0ebca67 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -111,7 +111,6 @@ it lands before the cut. - [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it - [Moxygen minimal interop](/quest/m1/smoke-moxygen.md) - waiting on Moxygen to send a registered filter type, then the smoke lane stops hiding its failure - [More tests under load](/quest/m1/test-flakes-2/README.md) - the second round of load-only failures, one quest per flake, fixed at the cause -- [A public model](/quest/m1/public-model/README.md) - every model item a built-in session reaches is public, unnecessary, or marked deliberately private, so a custom publisher, subscriber, or relay builds from the public model alone - [Broadcast lifecycle](/quest/m1/broadcast-lifecycle/README.md) - broadcasts are online or offline with no finished state in moq-net, finished-broadcast retention is a fixed moq-mux policy, and each linger timer has one meaning - [JS interop tone](/quest/m1/interop-js-tone.md) - the `js ->` cells pass the audio check on every draft again, after the 2026-10-23 regression - [A smaller, safer API than release](/quest/m1/api-audit/README.md) - every surface added since release has a consumer or is gone, overlapping operations are collapsed, names mirror across Rust, JS, and the bindings, and every break has its MIGRATE entry