Skip to content

feat: ADR-W018 L2 — live add/edit of network sources (the DEV-stack spine) - #181

Merged
aperim-agent merged 10 commits into
mainfrom
feat/w018-live-add-network-source
Jun 18, 2026
Merged

feat: ADR-W018 L2 — live add/edit of network sources (the DEV-stack spine)#181
aperim-agent merged 10 commits into
mainfrom
feat/w018-live-add-network-source

Conversation

@aperim-agent

@aperim-agent aperim-agent commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

ADR-W018 Level 2 — live add/edit of network sources (the DEV-stack spine)

Runtime-add a network ingest source to a running multiview — spawn the ingest producer, normalize/jitter it, place it into a tile — without disturbing the output. Network/file kinds (rtsp/hls/ts/srt/rtmp/rist/file) now apply live on the running full-pipeline engine, through one uniform ingest path. This is the bottom of the DEV-stack and the API PR #179's deferred e2e chroma test needs.

What it lands

  • Pipeline::live_ingest_spawner() + LiveSourceHub::start_with_ingest(...) — the production API: the hub gains an IngestSpawner seam; the full-pipeline run wires the pipeline's spawner, the software run wires None.
  • LiveSourceCapability { synthetic, network } on the control AppState (+ with_live_sources, is_live), re-exported from multiview_control; the binary derives it from ingest.is_some() so the X-Multiview-Apply header never over-claims live.
  • SourceKind::is_network_media() in the config schema — the level-2 classification point (disjoint from is_synthetic; ndi/youtube/aes67 in neither → restart). rist is included (it lowers to a rist:// AVIO URL and rides the identical ingest path).

Re-authored against current main (not cherry-picked)

The original PR #108 commits (23ca6955 RED, afe6ca68 impl) 9-way-conflict — pipeline.rs churned ~49× on main since. I read them as the intent, then re-implemented against current main's structures:

  • Main had already absorbed much of the live-add foundation (the synthetic-only LiveSourceHub, command_drain_with_live_sources, shared_stores, the upsert_source drain arm that explicitly held the network path as "the next ADR-W018 slice"). This PR is that next slice.
  • spawn_ingest_producer is extracted from IngestSupervisor::start so the startup supervisor and the live hub call the same function — a runtime add runs exactly the startup supervised ingest_loop (same reconnect bracket, jitter, PTS-normalize, rw-timeout), never a second-quality copy. Main's loop had also grown a #[cfg(youtube)] re-resolve sibling since feat(cli,control,web): network-source live add — runtime ingest spawn with placement consult (ADR-W018 level 2) #108 — preserved, re-ordered to spawn before the extracted producer.
  • The original's standalone LiveSourceCapability coexists cleanly with main's later LiveApplyCaps (overlays, ADR-W022) — two parallel per-collection capability signals on AppState.

Invariant #1 (output-clock) — the live-added source is SAMPLED, never pacing

drive_streaming keeps emitting one frame/tick regardless of the new ingest. The drain only does cheap binding mutations at the frame boundary (store create/reuse + route key + config mirror); the heavy half (libav open + thread spawn + placement poll) is handed to the off-thread LiveSourceHub over a bounded channel. The hub worker never touches the output clock. Soak proof: live_added_file_source_goes_live_then_remove_slates_never_faltering (ffmpeg) drives add → LIVE → remove → NO_SIGNAL → rapid add/remove churn on a real pipeline and asserts !report.faltered.

Invariant #10 (isolation) — one actor, bounded drop-oldest, no back-pressure

The ingest is one hub-owned producer thread feeding its per-source lock-free TileStore (per-source isolation, no head-of-line blocking). The drain→hub channel is bounded try_send (full ⇒ shed + warn, tile rides the slate); the control/add path cannot back-pressure the engine. Admission goes through the cost model: select_live_decode_pick (ADR-W018 §7) re-polls NVML and consults the same select_device scorer with the candidate set restricted to the running island's device — a runtime add never fragments or migrates the GPU pipeline; an over-headroom/vanished island rejects to software decode, never overcommits. Chaos proof: the same churn test + the gpu placement unit tests (live_decode_pick_admits_onto_the_idle_island..., ..._rejects_an_over_headroom_island_to_software, ..._degrades_to_software_when_the_island_vanishes, live_spawner_consults_the_admission_path_on_every_decoded_spawn — exactly one fresh load poll per spawn).

Restores PR #179's deferred e2e test

crates/multiview-cli/tests/live_decode_chroma.rs (deferred from #179 because it referenced the not-yet-built live_ingest_spawner/start_with_ingest) now compiles + runs + passes on the live-add path: a live-added decode's U/V plane statistics match the startup decode of the same clip (the Defect-A U/V-inversion pin).

Gate evidence (lane-local target, env -u CARGO_TARGET_DIR)

  • cargo fmt --all -- --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — exit 0
  • cargo clippy -p multiview-cli --features ffmpeg --all-targets -- -D warnings — exit 0 (the ffmpeg-gated leg that caught fix(ffmpeg): bypass libswscale for identity NV12 scale (hw Defect A) #179's CI failure)
  • cargo clippy -p multiview-cli --features ffmpeg,overlay,gpu --all-targets -- -D warnings — exit 0 (compiles the gpu-gated placement code)
  • cargo test --workspace — exit 0
  • cargo test -p multiview-cli --features ffmpeg (e2e) — both realtime proofs pass; gpu placement unit tests 8/8; control typed_resources 23/23 (incl. 5 new capability-matrix tests)
  • cargo deny — not required (no Cargo.toml/Cargo.lock changes)

HIGH-risk data-plane — not self-merging. Requesting the 3-lens cross-vendor panel (rule 21).

🤖 Generated with Claude Code


Pre-panel close-outs (orchestrator review items)

1. Inert cost-budget — verified + residual disclosed. select_live_decode_pick (and the pre-existing select_admission_pick) use CostBudget::new(100_000, 100_000, 100_000), an intentionally permissive per-engine Mpix/s budget. I verified the deferral claim: the budget being a near-inert gate with the VRAM-headroom hard gate + VRAM-dominant score as the real safety is documented in ADR-0018 ("hard gates are the real safety"; the cost model is "biased conservative"). NOTE: the pre-existing comment cited "ADR-0035 §5" for this — that citation is wrong (ADR-0035 has no §5 and does not scope a per-GPU perf-class budget table); commit 058b1fc corrects it to ADR-0018 and states the perf-class table is genuine future work (not yet built). Disclosed residual: a VRAM-roomy but decode-engine-saturated GPU passes both the permissive budget and the 0.85 headroom gate, so a live add CAN be admitted onto a GPU that cannot sustain another decode. Why acceptable for this ship: the never-off-air contract holds — an over-subscribed decode degrades the new tile only (the output clock samples last-good; the program is never touched — inv #1/#2), and invariant #9's closed-loop degradation sheds the cheapest-impact tile if the saturation bites. The reject→software-decode ladder (select_live_decode_pick returns None ⇒ software open) already covers VRAM/session/headroom exhaustion; only the decode-Mpix dimension is un-budgeted until the perf-class table lands.

2. Management-completeness (§H) — confirmed, no new operator control needed (one copy fix). W018-L2 (live add/edit of a network source) is fully reachable through the existing source-CRUD UI: web/src/pages/SourcesPage.tsx already offers every network/file kind (rtsp/hls/ts/srt/rtmp/file/youtube) with typed locator + RTSP-transport fields, and surfaces X-Multiview-Apply generically — so the server-side header flip needs zero new web wiring. The only web change required was honesty copy (rule 27): the apply-semantics callout + saved-description previously said network/file sources "go live via config export + restart", which my server change makes false. Commit 058b1fc refreshes that copy (synthetic + network/file apply live on a full-engine run; NDI/YouTube/AES67 and the software engine restart). No new API resource or UI control is missing.

3. Full fresh gate re-run (lane-local target, env -u CARGO_TARGET_DIR, -j1 for Rust):

  • cargo fmt --all -- --check → exit 0
  • cargo clippy --workspace --all-targets -- -D warnings → exit 0
  • cargo clippy -p multiview-cli --features ffmpeg --all-targets -- -D warnings → exit 0
  • cargo clippy -p multiview-cli --features ffmpeg,overlay,gpu --all-targets -- -D warnings → exit 0
  • cargo test --workspace → exit 0 (a cold parallel build first hit the known spurious E0463 can't find crate multiview_input race in an untouched crate; a warm-cache retry passed clean)
  • cargo test -p multiview-cli --features ffmpeg → the two e2e realtime proofs pass; gpu placement 8/8; control typed_resources 23/23
  • web (since web/ changed): npm ci; eslint --max-warnings=0 exit 0; tsc --noEmit exit 0; lingui compile exit 0; vitest run 632 passed / 63 files exit 0
  • cargo deny — not required (no Cargo.toml/Cargo.lock changes)

CI attribution: on the prior CI run both check + test jobs (macos + ubuntu) passed. The two red jobs were NOT attributable to this PR: web (eslint+tsc+vitest+build) fails on main at the same base SHA (pre-existing — locally the full web suite passes 632/632 here, so it is a CI-env flake in the WhepPlayer media tests), and feature-gated clippy (multiview-webrtc, native) is a flaky timing test (servicing_keeps_pace_with_the_receive_budget_under_flood) in an untouched crate that passed on re-run.


Correctness fix (commit 943706a) — DecodePlacement tri-state

Orchestrator review caught that my initial re-author flattened the 2a67e4e4 DecodePlacement amendment into Option<String> — a real overcommit/fragmentation bug, now fixed.

The bug: select_live_decode_pick returned Option<String>, so a placement reject returned None, leaving IngestPlan.cuda_ordinal = None. In open_and_stream that becomes new_preferring_hw(.., want_hw=true, None) → NVDEC opens on libav's default CUDA device. On a single-GPU over-headroom island the default device IS that island (overcommit); on a multi-GPU host it may be a different GPU (silent island fragmentation, forbidden by ADR-0018). "No ordinal" cannot distinguish no placement decision (default device fine) from placement rejected (hardware must NOT open).

The fix: an explicit, closed tri-state DecodePlacement::{Default, Pinned(ordinal), SoftwareOnly} on IngestPlan + a single decoder_open_args gate: SoftwareOnly → (want_hw=false, None) forces software even when NVDEC is compiled/present/not env-disabled; Pinned/Default keep the canonical want_hw_decode env reading (operator MULTIVIEW_DISABLE_NVDEC still wins over a pin). select_live_decode_pick → select_live_decode_placement: admit → Pinned (or Default when the island resolved no ordinal); reject AND island-vanished → SoftwareOnly. Restored tests/decode_placement.rs (the f0a6b7f/2a67e4e4 RED pin), which opens the real decoder via the gate's exact args and asserts a SoftwareOnly source comes back software (used_hw=false, no cuvid named); the gpu-lane unit tests now assert the placement value AND the decode-open behaviour per placement, not just the picked ordinal. ADR-W018 §7 amended for the as-built tri-state.

Re-gated after the fix (all green, lane-local target, env -u CARGO_TARGET_DIR, -j1 for Rust): cargo fmt --all -- --check exit 0; cargo clippy --workspace --all-targets -- -D warnings exit 0; cargo clippy -p multiview-cli --features ffmpeg,overlay,gpu --all-targets -- -D warnings exit 0; cargo test --workspace exit 0; cargo test -p multiview-cli --features ffmpeg,overlay,gpu: decode_placement 2/2, gpu placement units 8/8, live_network_apply + live_decode_chroma 1/1 each.


Panel-blocker resolutions (head 1abed23)

The Codex 3-lens panel reviewed head 058b1fcb (before the tri-state fix) and returned two findings; both are now resolved.

Blocker #1 (tri-state to the open path) — fixed in 943706a (the "DecodePlacement tri-state" section above). The panel saw the old Option<String> code where a reject's None opened NVDEC on the default device. Now DecodePlacement::{Default,Pinned,SoftwareOnly} threads through IngestPlan to the open path, SoftwareOnly → decoder_open_args → (want_hw=false, None), and tests/decode_placement.rs asserts SOFTWARE behaviour at the open path (the real new_preferring_hw call → used_hw=false, no cuvid named), not just that the pick returns None.

Major #2 (RIST capability gating) — fixed in 1abed23. RIST was advertised live-appliable unconditionally, so an ffmpeg-without-rist build over-claimed X-Multiview-Apply: live for a RIST source whose spawn would refuse (tile rides slate). Fixed: LiveSourceCapability gains a rist: bool the binary sets from cfg!(feature = "rist") (via with_rist); is_live returns self.rist for SourceKind::Rist, self.network for the other network kinds. is_network_media classification is unchanged (RIST is network media); only its live-apply capability is feature-gated — the honest level. Two new typed_resources tests pin both directions (with_rist(false) ⇒ RIST restart + no UpsertSource while a non-RIST network kind still flips live; with_rist(true) ⇒ RIST live + UpsertSource).

Re-gated after both fixes (all green, lane-local target, env -u CARGO_TARGET_DIR, -j1 for Rust): cargo fmt --all -- --check exit 0; cargo clippy --workspace --all-targets -- -D warnings exit 0; cargo clippy -p multiview-cli --features ffmpeg exit 0 AND --features ffmpeg,rist exit 0 AND --features ffmpeg,overlay,gpu exit 0 (all cfg branches compile); cargo test --workspace exit 0; control typed_resources 25/25; decode_placement 2/2; gpu placement units 8/8; live_network_apply + live_decode_chroma 1/1 each.

verify and others added 10 commits June 17, 2026 04:13
…rk live add

TDD-first: failing tests for network source live add/edit functionality.
Tests expect APIs not yet implemented:
- typed_resources.rs delta: LiveSourceCapability + with_live_sources + capability-driven header
- live_apply_header_http.rs: bind_and_serve HTTP honesty (live only when spawner wired)
- live_network_apply.rs (ffmpeg-gated): e2e real ingest loop + state machine + never-falter proof

These tests pin the exact behavior: network/file kinds live on full-pipeline,
ndi/youtube/aes67 always restart, capability derives from ingest.is_some().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k live-add

- config/schema.rs: add SourceKind::is_network_media() classifier (rtsp/hls/ts/srt/rtmp/file/rist)
- control/state.rs: add LiveSourceCapability struct {synthetic, network} with constructors + is_live(kind)
- control/state.rs: add AppState.live_sources field (default synthetic_only)
- control/state.rs: add AppState::with_live_sources() builder
- control/lib.rs: re-export LiveSourceCapability

These provide the run-path capability signal the control plane threads to declare
which source kinds the engine can live-apply, ensuring X-Multiview-Apply headers
stay honest per build/run path (synthetic-only on software, synthetic+network on
ffmpeg).

BREAKING: bind_and_serve and related functions will need the live_sources param
in next commits; routes/sources.rs will call state.live_sources.is_live().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changes live_apply_upsert to call state.live_sources.is_live(kind) instead of
just checking is_synthetic(). This allows network/file kinds to be live-applied
when the capability is set (full-pipeline run), and keeps ndi/youtube/aes67
always restart.

Also updates the kind-change check to use is_live() for both synthetic->network
and network->non-live transitions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m ingest path

Network/file kinds (rtsp/hls/ts/srt/rtmp/rist/file) now apply LIVE on the
running full-pipeline engine through ONE uniform ingest path — the level-2 spine
of the DEV-stack, re-authored against current main (the original PR #108 commits
9-way-conflict; pipeline.rs churned 49x since).

- cli live_sources: SourceSpawn + IngestSpawner seam + SpawnedProducer;
  LiveSourceHub::start_with_ingest threads the run's spawner; the worker's
  SpawnSource arm tears down any prior producer (edit = store-reuse swap) then
  spawns through the seam; no spawner => held + warned (slate, inv #10).
- cli pipeline: spawn_ingest_producer extracted — IngestSupervisor::start and
  the new LiveIngestSpawner call the SAME fn, so a runtime add runs exactly the
  startup supervised ingest_loop (reconnect/jitter/PTS-normalize/rw-timeout).
  Placement (ADR-W018 §7, gpu): drive_streaming publishes the pinned LiveIsland;
  select_live_decode_pick re-polls NVML and consults the SAME select_device
  scorer with the candidate set restricted to the island device (never frags/
  migrates) — admit => island ordinal stamped, reject/island-vanished => that
  source decodes in software, warned. The output never falters (inv #1).
- cli control: bind_and_serve takes the run's LiveSourceCapability; upsert_source
  routes a network kind to request_spawn_source (store create/reuse + route key
  + config mirror at the frame boundary, heavy spawn off-thread BEFORE the
  rebind — inv #1/#10); ndi/youtube/aes67 held with the reason.
- cli main: ControlPlaneWiring carries the ingest spawner; the capability sent
  to bind_and_serve is DERIVED from ingest.is_some() — the header claims live
  for network kinds exactly when a real spawner backs it. Pipeline path wires
  Pipeline::live_ingest_spawner(); software path wires None.
- control routes/sources: live_apply_upsert answers X-Multiview-Apply from the
  run capability; OpenAPI + doc updated. ADR-W018 amended for the as-built L2.

Restores the e2e chroma pin PR #179 deferred (live_decode_chroma.rs): a
live-added decode's U/V plane stats match the startup decode of the same clip.

Gates (lane-local target, env -u CARGO_TARGET_DIR, -j1): fmt clean; clippy GREEN
on default, ffmpeg, and ffmpeg+overlay+gpu; control typed_resources 23/23;
8 gpu placement unit tests incl the seam-pin spy; both ffmpeg e2e realtime proofs
GREEN (live add reaches LIVE, remove slates to NO_SIGNAL, churn never falters).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…turation residual; refresh SourcesPage L2 copy

Pre-PR review close-outs (orchestrator-flagged):

- pipeline.rs: the `select_admission_pick` budget comment cited "ADR-0035 §5" for
  the per-GPU perf-class CostBudget table being a future refinement — that
  citation is WRONG (ADR-0035 has no §5 and does not scope the budget table).
  Corrected to cite ADR-0018's hard-gate principle ("hard gates are the real
  safety": VRAM headroom + capability + NVENC-session), and state the perf-class
  table is future work (not yet built) — rule 27, no aspirational citation.
- pipeline.rs: `select_live_decode_pick` now DISCLOSES the known residual inline
  — a VRAM-roomy but decode-engine-saturated GPU passes both the permissive
  Mpix/s budget and the headroom gate, so a live add CAN be admitted onto a GPU
  that cannot sustain another decode. Acceptable for this ship: the never-off-air
  contract holds (an over-subscribed decode degrades the NEW tile, never the
  program — inv #1/#2, the clock samples last-good) and inv #9's closed-loop
  degradation sheds the cheapest tile if it bites. Comment-only; no code change.
- web SourcesPage: the apply-semantics callout + saved description said network/
  file sources "go live via config export + restart" — now FALSE after L2 (they
  apply live on a full-engine run). Refreshed the copy to match: synthetic +
  network/file apply live on a full-engine run; NDI/YouTube/AES67 (and any kind
  on the software engine) restart. The UI already reads the real
  X-Multiview-Apply header — this is the explanatory copy catching up (rule 27).

Web gate (npm ci then): eslint --max-warnings=0 EXIT 0; tsc --noEmit EXIT 0;
lingui compile EXIT 0; vitest run 632 passed / 63 files EXIT 0 (incl the 2
SourcesPage suites 10/10). Rust: fmt clean; clippy -p multiview-cli
--features ffmpeg,overlay,gpu EXIT 0 (the comments compile clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ive decode forces software (2a67e4e)

Carries the 2a67e4e amendment my initial re-author flattened away — a real
overcommit/fragmentation bug, not a refinement.

THE BUG (in my prior commit): select_live_decode_pick returned Option<String>,
so a placement reject returned None, leaving IngestPlan.cuda_ordinal = None. In
open_and_stream that means `new_preferring_hw(.., want_hw=true, None)` — NVDEC on
libav's DEFAULT CUDA device. On a single-GPU over-headroom island the default
device IS that island (overcommit); on a multi-GPU host it may be a DIFFERENT
GPU (silent island fragmentation, forbidden by ADR-0018 never-fragment). "No
ordinal" cannot distinguish *no placement decision* (default device fine) from
*placement rejected* (hardware must NOT open).

THE FIX: an explicit, closed tri-state DecodePlacement::{Default, Pinned(ordinal),
SoftwareOnly} on IngestPlan + a single decoder_open_args gate:
- SoftwareOnly -> (want_hw=false, None): hardware never attempted, even when
  NVDEC is compiled/present/not env-disabled.
- Pinned(ordinal)/Default -> keep the canonical want_hw_decode env reading
  (MULTIVIEW_DISABLE_NVDEC opt-out still wins over a pin); Pinned threads the
  island ordinal.
- select_live_decode_pick -> select_live_decode_placement: admit -> Pinned (or
  Default when the island resolved no ordinal); reject AND island-vanished ->
  SoftwareOnly, with truthful FORCING-software warnings.
- drive_streaming stamps Pinned; ingest_plan_for defaults Default; open_and_stream
  routes through decoder_open_args.

TDD: restored tests/decode_placement.rs (the f0a6b7f/2a67e4e4 RED pin) — it
opens the REAL decoder via the gate's exact args and asserts a SoftwareOnly
source comes back software (used_hw=false, no cuvid named); the gpu-lane unit
tests now assert the placement value AND the decode-open behaviour per placement
(not just the picked ordinal). ADR-W018 §7 amended for the as-built tri-state.

Gate (env -u CARGO_TARGET_DIR, lane-local, -j1): fmt clean; clippy --workspace
EXIT 0; clippy -p multiview-cli --features ffmpeg,overlay,gpu EXIT 0;
decode_placement 2/2, gpu placement 8/8, live_network_apply + live_decode_chroma
1/1 each (ffmpeg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er-claim X-Multiview-Apply:live (panel major)

Codex panel (#181) MAJOR: RIST was advertised live-appliable unconditionally —
`is_network_media()` includes `Self::Rist` and the binary derives
`LiveSourceCapability::synthetic_and_network()` from `ingest.is_some()`. On an
`ffmpeg`-WITHOUT-`rist` build the hub wires a real ingest spawner (so
rtsp/hls/ts/srt/rtmp/file correctly flip to `live`), but a RIST source's
`ingest_plan_for` arm is a typed refusal (`librist` not built) — so the header
claimed `X-Multiview-Apply: live` for a spawn that refuses, and the tile rode
the slate. The header over-claimed.

Fix: `LiveSourceCapability` gains a separate `rist: bool` the binary sets from
`cfg!(feature = "rist")` (via the new `with_rist` builder); `is_live` returns
`self.rist` for `SourceKind::Rist` and `self.network` for the other network
kinds. `synthetic_only` ⇒ rist:false; `synthetic_and_network` ⇒ rist:true (the
canonical "full network" meaning), narrowed by the binary's
`serve_control_plane` to the build's actual rist truth. RIST classification
(`is_network_media`) is unchanged — RIST IS network media; only its *live-apply
capability* is feature-gated, the honest level.

Tests (control typed_resources, 23→25): `rist_stays_restart_when_the_engine_
build_lacks_the_rist_feature` (with_rist(false) ⇒ RIST restart + no UpsertSource,
while a non-RIST network kind still flips live) and `rist_applies_live_when_the_
engine_build_carries_the_rist_feature` (with_rist(true) ⇒ RIST live + UpsertSource).

Gate (env -u CARGO_TARGET_DIR, lane-local, -j1): fmt clean; clippy --workspace
EXIT 0; clippy -p multiview-cli --features ffmpeg EXIT 0 AND --features
ffmpeg,rist EXIT 0 (both cfg branches compile); control typed_resources 25/25.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…startup producers

Codex panel round-2 on #181 found three real data-plane defects beyond the round-1 tri-state; all fixed here:

- EMPTY-ISLAND fall-through (blocker): LiveIngestSpawner consulted select_live_decode_placement only when an island was published; an empty slot (startup admission rejected the GPU / no NVML) kept DecodePlacement::Default -> default-device NVDEC. decode_placement_for now forces SoftwareOnly when admission was attempted but named no island (fail closed, ADR-W018 §7).
- ADMIT-with-no-ordinal (major): an island that passes budget but resolves no CUDA ordinal returned Default -> default-device NVDEC on the LIVE path (the compositor island is already pinned). Now forces SoftwareOnly.
- TWO-WRITER race on a reused single-writer TileStore for a live EDIT of a STARTUP-origin source (blocker): teardown only joined hub-owned producers; a startup producer JoinHandle lives in IngestSupervisor, so the old decode thread could still write while the replacement published. register_stop now returns an exited latch every producer carries as an ExitGuard (flips on return or panic); teardown raises stop then bounded-waits on the latch (await_exits) before the replacement publishes -- covering startup-origin producers, not just hub-owned ones.

ProducerStop.stop is pub(crate) so the cross-module registration tests raise it; the gpu-gated placement matches are if-let-else (clippy single_match_else). Gate (lane-local, env -u CARGO_TARGET_DIR, -j1): fmt + clippy --workspace + clippy -p multiview-cli --features ffmpeg,overlay,gpu + test -p multiview-cli --features ffmpeg + test --workspace all EXIT 0. Completes the stalled dev108-live-add lane WIP (round-2 authored there; the 5 ExitGuard call-site completions + the ProducerStop accessor + clippy/test fixups by the integrator).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… orphans the latch (round-2 panel)

Codex round-2 panel on #181 flagged the ExitGuard-on-spawn-failure gap: register_stop inserts the stop entry + returns the exited latch BEFORE Builder::spawn, but the ExitGuard that flips exited was constructed INSIDE the closure — so a (rare) thread-spawn Err left an orphaned exited=false entry, and a later teardown of that id busy-waited the full TEARDOWN_JOIN_GRACE (3s) before detaching (a regression from the round-1 no-op join; bounded + off the output-clock thread, never an inv #1/#10 breach).

Fix: construct the ExitGuard BEFORE the spawn and move it into the closure at all 7 producer-spawn sites (run.rs synth generator; pipeline.rs audio/tone/captions/youtube-reresolve + the spawn_ingest_producer decode thread; live_sources.rs hub synth). Its Drop now flips exited whether the thread runs (drops on exit) OR Builder::spawn fails (the closure owning the guard is dropped) — so a failed spawn deregisters cleanly and teardown never busy-waits an orphan.

Gate (lane-local, env -u CARGO_TARGET_DIR, -j1): fmt + clippy --workspace + clippy -p multiview-cli --features ffmpeg,overlay,gpu + test -p multiview-cli --features ffmpeg + test --workspace all EXIT 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fault) on the live path + RIST live-apply is rist-feature-gated (round-3 panel)

Two doc-drift minors the round-3 Codex panel flagged (rule 27 — the ADR prose contradicted the verified code):
- §7 said an admitted live decode with no resolved CUDA ordinal stamps Default; the round-2 fix forces SoftwareOnly on the live path (Default is the startup placement, where nothing is pinned yet). Corrected.
- The level-2 amendment said rist is included unconditionally; the code gates RIST live-applicability on cfg!(feature=rist) (with_rist), keeping an ffmpeg-without-rist build at restart. Corrected.

Docs-only; the Rust build/clippy/tests are unaffected (green at 46ceea3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aperim-agent
aperim-agent merged commit 354835b into main Jun 18, 2026
47 of 48 checks passed
aperim-agent added a commit that referenced this pull request Jun 19, 2026
)

`transport::unified::tests::servicing_keeps_pace_with_the_receive_budget_under_flood`
flooded a loopback transport for a fixed 400ms wall-clock window, then asserted
`exhausted > 0` ("the flood must exhaust at least one receive budget"). On a
loaded/contended CI runner the dedicated flooder OS thread is CPU-starved, so
within 400ms it can exhaust ZERO budgets and the *precondition* assert fails —
a runner-load-dependent timing assertion, not a real defect. It reddened the
`feature-gated clippy (multiview-webrtc, native)` leg on main on at least two
pushes (PR #181 era, the 2026-06-19 #195 merge), each forcing a manual re-run.

Reproduced under CPU contention (2x nproc `sha256sum /dev/zero` hogs): ~1/30
runs fail, 100% of failures the `exhausted == 0` precondition at the old
assert; the discriminating invariant never fails.

Fix: drive the flood to a DETERMINISTIC number of exhausted budgets
(`TARGET_EXHAUSTIONS = 8`) by polling `RunProbe::recv_budgets_exhausted`
instead of sleeping a fixed window, then stop the driver. The flooder keeps the
socket permanently readable, so the driver reaches the target given any CPU at
all — the saturation precondition is now guaranteed regardless of runner load.
A generous 30s `timeout` still bounds a wedged regression (it can never reach
the target), so such a regression fails deterministically rather than hanging.

The discriminating invariant is UNCHANGED and in fact strengthened: still
`services >= exhausted` (servicing keeps pace with the receive budget under a
genuinely saturating flood — invariant #10), now proven against >= 8 exhausted
budgets (a sustained flood) rather than possibly a single trip. Test code only;
no production behaviour change.

Co-authored-by: verify <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