Решили: TapeChunkWriter.Write стал TryWrite(tape, firstTick, tickCount, maxBytes, out bytes). Предел приходит параметром, false означает ровно «не влезло», номер чанка тратится только на успешной записи. DiscardLast удалён за ненадобностью.

Почему: пределов было два — потолок формата (64 КБ, у писателя) и предел транспорта (у Steam 512 КБ, у стримера), — и реагировали они по-разному: писатель бросал исключение, стример делил диапазон пополам. Пока чанк оставался меньше 64 КБ, работало деление по пределу транспорта; чанк между 64 КБ и 512 КБ ронял исключение, не дав стримеру ничего решить. Отвергнутые альтернативы: поднять MaxChunkBytes до предела транспорта (одна строка, но остаются и throw как поток управления, и DiscardLast, и сгорающий номер) и «признать деление недостижимым» (сохраняет зависание раздачи).

Опора не только на вкус: конвенция .NET для TryFormat/TryWrite формулирует то же правило дословно — возвращать false следует ТОЛЬКО при нехватке места в буфере, любые другие сбои бросают, а вызывающий на false меняет размер и повторяет. У нас «повторить» — это поделить диапазон тиков.

Грабли: номер чанка увеличивался ДО проверки размера, поэтому исключение уносило его с собой: _nextTick не двигался, DiscardLast не вызывался, и каждый следующий кадр повторял то же исключение. Раздача вставала навсегда, а у гостя оставалась дыра в нумерации, которую он просил повторить до конца боя. Пока номер тратится раньше успеха, любой отказ ниже по коду означает вечную дыру — поэтому правильный порядок («сначала решение, потом расход») дороже любой обработки ошибок.

Владелец правды: TapeChunkWriter.TryWrite (контракт в <returns>), TapeStreamer.SendChunk (вычисление предела), тесты ChunkOverTheLimit_ReturnsFalse_AndKeepsTheChunkNumber и ChunkTooBigForTheTransport_IsSplit_NotDropped.