feat(host): a Hello that names a format but not the capability was ignored in total silence

Found on glass, and it cost the first run of the §13.2 check. The host resolved
`plane="0xC9 Opus"` while every condition a reader could see was satisfiable — the
operator policy was on, the session was stereo, the rate was supported, and the video
bitrate left room. The reason was gate condition 1, which is deliberately NOT logged
because "the client did not set the capability" is every ordinary session with every
shipping client and would drown the log.

That reasoning holds for the ordinary case and not for this one. A `Hello` carrying a
rate or a depth but no `CLIENT_CAP_AUDIO_HIRES` is contradictory: something asked, and
is being ignored. The two halves come from different places in a client — the capability
from a settings toggle, the format from whatever that toggle resolved to — so they can
drift apart, and an embedder that gets it wrong currently sees nothing at all.

One warn, only in the contradictory case, naming both halves and saying they must be set
together.
This commit is contained in:
2026-08-16 12:39:42 +02:00
parent 362be21ebb
commit a9ffd3becd
@@ -834,6 +834,23 @@ pub(super) async fn negotiate(
// wire/sequencing changes well beyond this pass; neither is needed for 48 kHz, where the
// conservative answer already lands on the longest rung.
let hires_asked = hello.client_caps & punktfunk_core::quic::CLIENT_CAP_AUDIO_HIRES != 0;
// A `Hello` that names a format but does not set the capability is CONTRADICTORY, and the two
// halves come from different places in a client — the capability from a settings toggle, the
// rate and depth from whatever that toggle resolved to. Condition 1 below is deliberately not
// logged, because "no capability" is every ordinary session with every shipping client and
// would drown the log. This case is not ordinary: something asked, and is being ignored.
//
// Worth the line because it is the exact shape that cost an on-glass session its first run —
// the host resolved Opus while every visible condition looked satisfiable, and the reason was
// unlogged by design. An embedder hitting this sees nothing at all otherwise.
if !hires_asked && (hello.audio_rate_hz != 0 || hello.audio_bits != 0) {
tracing::warn!(
requested_rate_hz = hello.audio_rate_hz,
requested_bits = hello.audio_bits,
"client sent an audio format but not CLIENT_CAP_AUDIO_HIRES — ignoring it and \
staying on Opus; the capability and the format must be set together"
);
}
let hires_allowed = pf_host_config::config().audio_hires.unwrap_or(false);
// §8.4 condition 4 — what the capture path can HONESTLY deliver, asked of the device rather
// than inferred from a successful open (§4.3/§4.4: both backends resample a rate they cannot