Repository navigation
chore: sync upstream AetherEngine 7.28.3 into the Silo fork - #16
Conversation
… (audit LIF-104) Only `load()` consumed the AE#597 flag. `reloadWithAudioOverride` (audio pick, custom disc-title pick, the custom-source reload) kept the native host and left the flag up, so the first such rebuild after a reset mounted its item on the invalidated AVPlayer. It now consumes the flag, drops the native host and the audio-only player like `load()` does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…dit NET-109, NAT-105, DEC-103, Vcred-101) HLSOriginRelay.admit both allowed an origin and made it a credential anchor, and the superuser404notfound#316 build admitted the post-redirect URL, so after a cross-host redirect the edge got the host's token and the host's own origin lost it. admit is split into allow(_:) and grantCredentials(to:httpHeaders:); the proxy grants the URL the host handed over and only allows the redirect target, and relayURL(for:) only allows. The playlist-driven fetchers that still sent every host header to every URI a playlist named now share one CredentialScope type (the NET-7 rule the ingest readers already follow): the superuser404notfound#316 proxy's variant fetch (anchor: the host's origin URL), the remote-HLS audio tap (anchor: the loaded URL) and the live subtitle rendition fetch (anchor: the live ingest reader's credential origin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…audit LIF-102) `reloadWithAudioOverride` ended in an unconditional `play()` and `.playing`, so an audio-track pick on a paused session resumed it, and on a custom source every `reloadAtCurrentPosition` (the audio-delay nudge, a superuser404notfound#460 correction, the AE#561 rebuild) did the same, including a mount with `autoplay = false`. The transport is now resolved before `.loading` (`sessionRebuildResumesPlaying`, or the value `reloadAtCurrentPosition` passes, which is the mount flag after a background teardown) and drives the stacking park, `play()` and the final state; a rebuild that does not resume is settled `.paused` by the host's readiness, like a paused mount (superuser404notfound#124). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…LS-101) Only the session's first producer got onLiveSegmentFinalized and liveResidentCapProvider (and the sequential twin), set in start() after the provider was built. The live reopen, the in-place muxerFailed rebuild and the AE#222 live rebuild all build through makeProducer, which wired neither, so every in-engine live recovery cut segments the playlist never listed: the media playlist froze at the dead producer's last segment while "live reopen succeeded" was logged. The wiring moves into wireProviderCallbacks. start() calls it once the provider exists, and makeProducer calls it for every producer built while a provider is installed, so no rebuild site can forget it. A live producer started without its report now trips an assert. Tests: a live session over an embedded 8 s MPEG-TS whose reader ends after one pass keeps growing its playlist after the reopen, and a producer rebuilt in place reports into the same provider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…HLS-102) The three plan builders converted durations and index timestamps with trapping `Int64(_:)` / `Int(_:)` and sized arrays from the duration. A container header can state about 9.2e12 s and a Matroska index can carry entries near both `Int64` extremes, so a hostile file trapped the plan (1 ns timebase), asked the uniform builder for 2.3e12 segments, or overflowed the trust check. - `Demuxer.effectiveDurationSeconds` clamps to one week, non-finite to 0. - `indexedKeyframes` and `firstIndexedTimestamp` drop entries past 2^62 ticks or 4e9 s, and do the AE#105 fold and the superuser404notfound#409 offset with checked arithmetic, dropping an entry that overflows. - The builders compute the trust-check spread in Double, go through a checked `planPts` helper that returns an empty plan on overflow, and the uniform builder widens its stride so a plan never exceeds 200,000 segments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…eturn (audit LIF-101, LIF-105) The CORE-2 owed decision settled on any move out of `.loading`. A native load passes through `.paused` (item readiness, inside the tvOS play gate) seconds before it returns, so on tvOS the teardown ran under the load and the host's `load()` threw `CancellationError`, the signal for "a newer load or stop took over". Sodalite takes that as a supersede and skips its observers and the Jellyfin start report, leaving a spinner up. `loadSession` and `reloadWithAudioOverride` now mark the generation in flight and settle the owed decision from their own exit. The decision still hops through a Task, so the host's continuation after `load()` wires its observers first, then the teardown runs. A decision made at didEnterBackground, on the settle hop or at the iOS grace expiry while a load is in flight is owed instead. The tvOS PiP close goes through the same funnel, so a window closed during an in-PiP load or seek is torn down once it settles instead of never (LIF-105). The bookkeeping is platform-neutral so the settle timing is testable on macOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…elpers (audit DMX-113, BIT-105, DMX-103) `Int64(seconds * 1e6)` and the stream-time-base form in `Demuxer.seek(to:)` and `seekBounded` trapped on NaN, infinity and anything past about 9.2e12 s, and several public entry points pass host values through unvalidated (`FrameExtractor.thumbnail/snapshot(at:)`, the SW and audio hosts' `load(startPosition:)`, `seek(to:)` during `.loading`). `byteEstimateTarget` and `byteEstimateCorrection` converted before their `min(fileSize, ...)` clamp, so an origin that inflated its `Content-Range` total trapped the subtitle side reader's byte-estimate fallback. - New `Demuxer.ticks(forSeconds:timeBase:)`: nil unless finite and under 2^62 ticks; both seeks and `FrameDecodeContext`'s target PTS go through it, and the seeks return false on nil. - The byte estimates compare the raw Double against the total before converting. - API boundary: `FrameExtractor` returns nil for a non-finite or negative time, `FrameCache` saturates its bucket, the SW and audio hosts ignore a non-finite start position, and the public `seek(to:)` ignores a non-finite target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… session alone (audit HLS-104, HLS-105, SEG-104) One identity rule for three ways a producer the session already replaced still acted on it. SEG-104 (B): a restart replaced while its first post-seek read was in flight could open its gate afterwards, record its EpochAxis (dropping the table at and above its index) and wake re-cut waiters with an epoch whose bytes are never stored. The installed producer's epoch is now mirrored under anchorShiftLock on every assignment of `producer`, and the gate-open and live rebase handlers are passed their producer's epoch and ignore any other. HLS-104: the superuser404notfound#377 metered revive slept 3 to 45 s and then re-anchored whatever producer was current, typically the working one a segment-driven restart had installed during the backoff. It now captures its dead producer weakly plus the session epoch and restarts only if both are still current. HLS-105: the AE#412 re-cut mark was dropped when the seek's 2 s gate wait timed out, while the restart it marked kept running; its late gate then recorded isRecut false and composed the backoff into the axis. The mark is now a RecutMark owned by the restart: performRestart binds it to the producer it installs at the marked index, a restart elsewhere keeps a pending mark and drops a bound one, the wait's timeout no longer touches it, and a newer seek drops a mark whose restart never installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…udit NET-104, DMX-106) A first fragment starting `00 00` takes its size from the next 32 bits, and `DVDSubpictureAssembler` reserved all of it, up to 4 GiB, then appended every later fragment of the stream until that size was reached. FFmpeg's `dvdsub_parser` would bound this, but the superuser404notfound#651 declared stream bypasses it. `Demuxer.readPacketLocked` also copied the assembler out of its dictionary before mutating it, so each fragment's append copied the partial unit at full capacity. The assembler now drops a unit stated above 1 MiB (a DVD SPU is at most 53,220 bytes; the cap keeps the HD-DVD units `dvdsubdec` decodes and the 70 KB unit the superuser404notfound#651 tests pin) and reserves at most 64 KiB up front. The demuxer mutates the assembler in place and sizes the joined packet with `Int32(exactly:)`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…tes (audit SEG-102) While the video gate waits, every selected-audio packet is buffered with its own `AVPacket`, and the only bound was 8 MiB of payload. A zero-size packet was charged nothing and a 1-byte one one byte, so a crafted Matroska of EBML-laced 1-byte frames behind a video track that never flags a keyframe pinned about 8.4 million packets (1.3 to 1.5 GB of heap) while the VOD gate waited without a time bound. `shouldBufferPregateAudio` gains a defaulted `bufferedCount` / `maxEntries` (65,536) and the call site passes the buffer's count. The cap sits above what the byte cap admits for AAC or AC-3 and still covers about 22 minutes of 20 ms Opus frames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…SEG-104) The pump checked shouldStop only at the top of its loop, before it read. A read already parked when the session stopped the producer (the superuser404notfound#79 restart aborts it with markClosed, which Matroska, MPEG-TS and fMP4 report as end of file) came back as source EOF: the pump exited with .eof, flushed the bridge and adopted its half-written segment into the shared cache under the full index, where a later backward seek hit a short segment. The pump now re-checks right after every source read (after both reads of the side-audio merge) and frees the packet, and an .eof exit with a stop pending is remapped to .stopRequested. Making the closed demuxer itself refuse to report EOF is left to the demuxer package. Test: a Matroska pump parked mid-file, stopped, and then answered with end of file exits as a stop and adopts nothing (it exited .eof and cached the partial segment before). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…cessor (audit LIF-103, Vcore-101, Vcore-102) Session callbacks hop onto the main actor, and a hop that landed after the session ended acted on whatever owned the engine by then. The item death escalation queued behind a `stop()` spent the budget, sent an escalation event and flipped the stopped engine to `.error` through its rebuild's `sessionNotReloadable`; a load in that turn was rebuilt on the software path with the old failure. The HLS callbacks (shift, seek state, network phase, consumer re-engage, rebase, HDR10+) and the software and audio network-phase sinks wrote a torn-down session's state into the next. One generation-stamped hop (`hop(for:)`, `loadGeneration` is the session's identity on every path) now carries all of them, the escalation and the master fallback take the generation they were raised under, and the escalation's absorbed-failure decision asks the generation instead of `state`. A reused native host clears its pending display rejection and escalation at unload, since `@Published` replayed them into the next load's sinks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ow (audit SEG-107) `LoadOptions.dvrWindowSeconds` reached `Int(ceil(window / cadence))` in `LiveWindowSizing.requestedSegmentCount` unvalidated, so a host that expressed "keep everything" as `.infinity` (or passed NaN, or anything past about 4.6e18 s) trapped on the first live manifest request and on the pump's resident park cap. The same class 078f1be fixed in `wholeSecondsCovering`. The sizing now reads the window through `effectiveWindowSeconds`: NaN or nil is the live-only floor, the rest is clamped to [0, one day], which is far deeper than the 900-entry playlist ceiling ever serves and keeps the clamp log's `requested x cadence` product in range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… NAT-101, DEC-101, FEA-101, SUB-105) SEG-1 sanitized packet timestamps inside the loopback segment producer only. The software host, the audio-only host, the still extractor, the recording tap, the subtitle stores and every probe read the same Demuxer and got matroskadec's raw uint64 cluster times and INT64_MAX block durations, so a crafted file still trapped the process in the SW-host fold, the DVR feeder, the decoders' `pts * num` and the subtitle drain. `SourceTimestampBounds.sanitize(_:timeBase:)` unsets pts/dts (and zeroes the duration) at |ticks| >= 2^62 or |seconds| >= 4e9. `readDemuxedPacketLocked` applies it right after `av_read_frame`, before the superuser404notfound#407 clearing, the AE#105 fold, the superuser404notfound#651 join and the superuser404notfound#409 repair, and `readPacket` applies it again at every exit, because superuser404notfound#409 rewrites pts/dts after the funnel with wrapping arithmetic. The sidecar `SubtitleDecoder` runs its own read loop and gets one call there. Epoch-anchored 1 GHz timestamps stay under 2^62 until about 2116 and epoch seconds under 4e9 until about 2096. The producer keeps its own 2^60 bound unchanged. Since 4e9 x (2^31 - 1) < 2^63, a bounded tick times any AVRational numerator fits Int64. Values made after the funnel still get local checks: the decoders build their CMTimes through one checked helper (.invalid on overflow), the SW-host fold converts and applies its offset with checked arithmetic (unset on failure), and the DVR feeder and the still extractor turn ring seconds back into ticks through a checked conversion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ng it (audit NAT-106) `mSampleRate` is a Float64 AVFoundation fills from the container, and a QuickTime SoundDescriptionV2 carries it verbatim, so on the remote-HLS bypass the origin controls it. `RemoteHLSStreamDescription.audioTracks` checked only `isFinite` before `Int(_:)`, and the readyToPlay track dump in `NativeAVPlayerHost.audioFormatDescription` checked nothing, so NaN, infinity or a rate past `Int.max` trapped at every native readyToPlay. One helper, `RemoteHLSStreamDescription.wholeSampleRate`, returns the rate as an Int when it is finite and in [0, 1e9), else 0, and both sites use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…SUB-105) The demuxer bound covers packets, but cue times also arrive from live WebVTT renditions, external stores and a host calling the public `ASSScriptBuilder.timestamp`, and each of them scaled a Double into an Int with a trapping conversion. `SubtitlePacketStore.harvestChunk`, which the pump, the software-host tap and the prefetcher all store through, now applies the demuxer's rule: a non-finite or implausible time is unset, such a duration is 0. The drain's packet rebuild converts through a checked helper, and the cue store's dedupe key, the ASS and the WebVTT formatters pin NaN to 0 and anything past 4e9 s to that edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ic leaves Int64 (audit BIT-101) After a seek the landing keyframe re-anchored the sequence with a plain `dts - landingIndex * plan.step`. In fragmented MP4 the step comes from the 64-bit `tfdt` and the landing picture order can sit near -2^31 through `delta_pic_order_cnt_bottom`, so a crafted file trapped the demux thread on seek or resume. The product and the difference are now checked; on overflow the picture stays unrepaired and the next keyframe re-anchors. `presentationShift` subtracts with the same check and falls back to no shift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… total (audit SEG-101) A packet with neither dts nor pts advanced by its own duration with no cap, so nine crafted BlockDurations just under the producer's 2^60 bound walked the synthesized dts to Int64.max, and `OutputTimestampSanitizer` trapped on the next `last + 1`. `repairStrideTicks` caps every arm at one second of ticks, the cap `updatedStrideTicks` already applies to a learned stride, and a packet whose synthesized dts would leave the plausible range is dropped until a genuine timestamp re-anchors the stream. The sanitizer returns nil when no dts after the last one exists and the muxer drops that packet. The dts bumps, the buffered-fragment bound and the no-cut watchdog's advance use checked or saturating arithmetic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…-end budget (audit LIF-106, DEC-105) Both `waitForSwitch` loops polled with `try? await Task.sleep`, so a load whose task the host cancelled (Sodalite on Back and on every zap) spun the main actor until the switch cap, and a superseded load kept polling to its end. Each tick now goes through `gateTick`, which returns on cancellation or when the caller's `isCurrent` (the load generation at all four call sites: pre-flight, the superuser404notfound#667 wait, the play gate and the reload play gate) says the load is gone, and logs the abandonment. The 6 s observed-end cap was per gate. The superuser404notfound#667 wait leaves the record unspent for the play gate, so on a panel whose end is never announced or whose in-progress flag sticks both spent the full cap on one switch. The first gate that waits on an armed switch now sets a deadline, and a later gate on the same arm gets what is left of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…exit 64 (audit OPS-110) `takeDoubleFlag` rejected NaN and infinity only, so `play --seconds 1e30` loaded and then trapped in the run loop's `Int(seconds)`; negative counts reached `0..<n` (`seektest --seeks -1`, `smbtest --reads -1`, `hlslive --seconds -1`), `hlslive --segments ""` built `1..<0`, and the host-call delays (`--switch-audio 0@<ms>`, `--reload-applying-at`) overflowed `UInt64(ms) * 1_000_000`. New bounded variants `takeDoubleFlag(_:in:from:)` and `takeIntFlag(_:in:from:)` exit 64 with a message outside their range (duration flags up to a week, counts up to a million), `hlslive` refuses an empty segment list, and the delays go through a saturating `sleepNanoseconds`. Verified by running each case: every one now exits 64, and the saturated delay no longer traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…UB-112) `SubtitleRectText.parsePositionTag` and `playRes(fromASSHeader:)` parse with `Double(_:)`, which accepts `nan`, `inf` and `1e999`. `\pos(nan,nan)` published a NaN `SubtitleTextPlacement.position` and `PlayResX: inf` normalized every position to NaN, breaking the documented [0, 1] placement contract; a UIKit host that lays out with it raises `CALayer position contains NaN` (Sodalite filters it, AetherPlayer ignores placement). Both now require finite values: a non-finite tag yields no position, so the cue falls back to its alignment, and a non-finite play resolution is no declaration, so the default applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…de query values (audit SUB-104, SUB-108, SUB-109, NET-114) The nameless matchers (registered secrets, encoded payloads, userinfo, Xtream paths) only matched raw bytes, so an upstream URL carried percent-encoded inside a wrapper's query (IPTV proxy, debrid) leaked its password in load url=, asset.url= and URLError dumps. A line that holds a valid escape now also runs them over a decoded view built once, and each hit maps back to whole escapes. The key matcher stays raw-only, so the NET-1 terminator rule is unchanged. A base64 blob may now start after - or _ (/v1-eyJ...). After = the terminators : ; , ) > end a value only where prose follows them, so a query password holding one goes whole; the header form keeps the wide set. Added the other backends' names (api-key, cookie, passwd, pwd, sessionid, session_id, authtoken, auth_token, refreshtoken, sessiontoken), Bearer and Basic credentials with a 16-byte floor, JSON and dictionary keys whose quote closes before the separator, and counted secret registrations. The key-initial check is a table instead of a Set, which keeps an escape-free line at the old cost despite the extra matchers. The four NET-114 sites log the URL error code and host instead of the NSError dump with its failing URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…dit NAT-103) An in-place swap reuses a player that is still `.playing`, and that status reaches the fresh item about 1 ms after the load, before it is ready. The timeControlStatus sink took it for the item rolling: it released the AE#629 clock hold before the mount seek landed, so the pre-landing readings reached the published clock, and it latched `hasEverPlayed`, so a swapped item failing at startup took the post-playback branch. Both now wait for `.playing` on a ready item (the predicate the AE#440 one-shot already used, renamed `playingIsThisItemsRoll`), and the readyToPlay sink re-asks when the mirror already reads `.playing`. The mount-seek and host-seek releases stay. `aetherctl play --host-calls playreload|pausereload` still ends `VERDICT: OK`, with the carried edge visible before readiness. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
`PacketTimingProbe.run` (behind `aetherctl pktdump`) subtracted consecutive dts from the probed file with a plain `-`, so a hostile file whose dts sit on opposite sides near 2^62 trapped the probe. The delta now goes through `subtractingReportingOverflow`; a step that does not fit is counted as non-monotonic and kept out of the histogram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…udit SUB-107, NET-114) The 128-bit path token is the capability that keeps LAN hosts off the 0.0.0.0 listener, yet every request line, the superuser404notfound#227 first-request line, NativeAVPlayerHost's load url= and asset.url= and the superuser404notfound#316 serving line logged it verbatim. HLSLocalServer registers the token with the log redactor in start() and unregisters it in stop() (registrations are counted, so two servers never release each other's), and the request line is now composed from the route after the token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…on check (audit LIF-107) The probe block wrote `sourceVideoWidth`, `sourceVideoHeight`, `sourceVideoPixelAspectRatio` and `lastDetectedVideoCodec` before the post-probe generation check. The AE#532 RPU audit is a second read that `stopInternal` cannot abort, so a load superseded inside it came back after its successor's prologue and wrote its own values over the reset; a successor that never probes (the remote-HLS bypass) kept them, and Sodalite reads a non-zero width as "has video". The values are now collected in the block and published after the check. The test origin can hold every request after a point, which is how the test parks the audit of a synthesized Dolby Vision Profile 5 over HDR10 source while the successor loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…on sequential origins (audit SEG-103) 7.22 made sequential-origin audio follow the video cutter (audioFollowsVideoCut) at the per-feed bridge site and the stream-copy sites, but the EOF bridge.flush() loop still routed its tail by plan time. When the last GOP crossed the last plan boundary the flushed tail opened the plan index the cutter never reached: the real final segment was closed as not final, its duration never arrived, and ENDLIST landed behind the segment before it, so AVPlayer ended the archive early. Both bridge sites now route through bridgedAudioSegmentIndex, so the live, sequential and plan rules live in one place. Test: a 16 s forward-only Matroska archive (embedded, Opus bridged to FLAC, keyframes at 0, 9 and 11 s under the uniform 4 s plan) lists all 160 video frames and its whole duration. Before, the playlist stopped after 9 s with 90 of 160 frames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ility rule (audit HLS-102) The two fix branches each introduced the same 2^62 ticks / 4e9 s bound. The index check now delegates to SourceTimestampBounds.plausible(_:timeBase:) so the rule has one definition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…n the log (audit NET-111) A connection that never presented the session token cost the host's log two unthrottled lines per request (the rejection and the 404), and its request line was attacker text split only on CRLF, so a bare LF forged a line of its own. Every line such a connection can cause (rejection, malformed or non-UTF8 request, oversize head, EOF or recv error mid-head) now shares one LogThrottle with a tally, the rejection and its 404 are one line, and request text is logged with C0 controls and DEL escaped (a stranger's cut at 256 characters). Local to the server, because EngineLog passes the multi-line playlist bodies this server logs on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…cting from it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ind a registered name Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, at any rate Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s tail on close Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dget Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mb-test pass Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, lock two fakes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silo Kody — review completeReview finished. Check the inline comments for findings and verify each suggestion against the code and tests. Reviewing changes in Silo
Review settingsReview OptionsThe following review options are enabled or disabled:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 580377004d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Head of Silo-Server/AetherEngine#16: since 1fda333a it fixes the bugs Kody found in upstream code the sync takes. For Silo: an HTTP source no longer crashes on a 206 with a non-positive Content-Range total, the audio-only host plays to the last sample below 1x, a crafted cue no longer stalls the main actor, and a delayed live revive no longer queues a stale restart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`sharedOutputRole` was listed in `loadIdentityFields`, but `refusedFields` never compared it, so `reloadAtCurrentPosition(applying:)` rebuilt a running multiview engine under the other role instead of throwing `loadIdentityNotCorrectable`. A new test flips every identity field alone and expects exactly that field refused, and fails when the list names a field the comparison does not cover. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silo Kody — review completeReview finished. Check the inline comments for findings and verify each suggestion against the code and tests. Reviewing changes in Silo
Review settingsReview OptionsThe following review options are enabled or disabled:
|
printableURL treated every loopback host as the engine, so a nativeRemoteHLS origin on localhost printed its query credential in SERVED. An engine server is a loopback address on a port the source does not use; the rest, a localhost origin and its absolute renditions included, is redacted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…runner CI paused after the park instead of inside the 0.25 s tail, and a resume after a host pause restarts a parked clock, so the test read 3.5 s on a 1 s file. The tail now plays at 0.25x, about a second of wall time, and a run that still misses it starts over. Without the fix the test still fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The same deferred release the other parked-reader tests have, so a time-limit cancel cannot leave the append thread and close() blocked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silo Kody — review completeReview finished. Check the inline comments for findings and verify each suggestion against the code and tests. Reviewing changes in Silo
Review settingsReview OptionsThe following review options are enabled or disabled:
|
…server A different loopback port did not make a URL the engine's: a localhost origin\ncan list an absolute rendition on another localhost port. The engine endpoint\nis now the mounted item's URL when it is a loopback URL other than the source\n(the no-stand-in route mounts the source itself), and only URLs on that\nscheme, host and port print raw.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silo Kody — review completeReview finished. Check the inline comments for findings and verify each suggestion against the code and tests. Reviewing changes in Silo
Review settingsReview OptionsThe following review options are enabled or disabled:
|
Head of Silo-Server/AetherEngine#16 after its review rounds: since 58037700 the engine library only adds a refusal for a sharedOutputRole change in a session correction, which Silo does not use. The rest is aetherctl, tests and docs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Merges upstream AetherEngine through
dd1d5f88(7.14.0 to 7.28.3, plus upstream's unreleased transport, audio-selection and subtitle-delay fixes) into the Silo fork. Fork changes that upstream now covers are retired; the rest are refitted onto upstream's new code.Supersedes #13: upstream AE#681 (7.25.0) releases the shared audio session only when the last engine leaves, and runs every engine's activation and release on one queue.
What changed
Retired, covered upstream
Kept, fitted onto upstream
HTTPRequestAuthorizationon upstream's sealed relay (NET-1). Static headers now follow upstream'sCredentialScope(NET-7, NET-109); resolver output stays the host's per-URL decision, as the resolver contract already says. Taking upstream also fixes two static-header bugs the fork had: credentials went to every allowed origin, and the subtitle proxy granted them to the post-redirect URL.maximumBytes) use upstream's held-body limit (NET-10). The resolver's playlist preflight keeps the 2 MB cap the static preflight has. Relayed fetches use upstream's per-fetch pending cap (NET-107) in place of the fork's high-water wait.nativeTimelineOffsetSeconds, ASS for local primary rendering, the 16-thread software decode cap, TrueHD Atmos to APAC, and CI's separate authorization test process.New
play()after the seek regardless of the play intent.sourceTimeon the remote-HLS bypass. Silo draws ASS fromsourceTime; with the deselect it stayed on item time, ahead of the picture after a seek on a transcode. The suppression is attached with the order AE#616's report gives (deselect, wait, suppress, reselect) and dropped right before any selection AVPlayer should draw.e773c661,2d221aaa). Selected earlier, it joined AVPlayer's startup buffer. With stall waiting disabled, that stopped playback in Silo's authorization playback tests on CI.1fda333a, from Kody's review).Fixes to upstream code, from Kody's review
These files were identical to upstream
dd1d5f88and are unchanged in upstream 7.30.0. Each fix is small and has a regression test that fails without it:Content-Rangetotal is zero or negative (9c50e0d6).,'or), and a registered secret that starts a credential name no longer hides the value after it (d261d747).4ec5ac92).8fc0a12f).3e2e4a16).e63a2d96).aetherctlredacts origin URLs and resolves served-VTT URIs against the playlist that lists them;smb-test's timed pass is linear; the burst-origin fixture rejects impossible arguments (94fab68e).Task.yield()ordering, race a closed socket, or race a fake's state (d45af9e3).sharedOutputRolechange. The role was listed as load identity but never compared (4ebc8eb0).aetherctlprints in full only URLs on the mounted item's own engine server, so a localhost origin's credential is redacted too (71234f57,31047b05), and the new ring test releases its parked writer on a cancelled wait (a01a6a72).7f5af614).One finding is answered with a comment instead (
9a92e8cf): a first seek's superuser404notfound#409 head sample can run past the seek budget. Both bounded versions I tried were worse, either settling the repair verdict on a partial sample or leaving the seek unpositioned, which the software host's resume ignores.Silo's matching change (picker fix and pin bump) is Silo-Server/silo-apple#683.
Test plan
swift test; iPhone 17 Pro simulator (iOS 27.0) and Apple TV 4K simulator (tvOS 27.0) with Silo built against this branch, against the dev server. Simulator audio output off.swift testin CI's two processes: the Swift Testing and XCTest suites pass on CI at31047b05(4134 tests locally at58037700); the authorization group's 56 tests pass. One of three local full runs failedIssue243ReaderRetentionTests, which measures process-widemalloctotals; it passed alone five times and in the other two full runs. Local runs of the other CI steps (docs links, packet-cache scripts, partial composition regression, transport probe, releaseaetherctl, iOS/tvOS test-bundle builds, visionOS build) pass.drawn by the host overlay (rendition suppressed)), AE#616 measured a 1.97 s lead off it, and only Silo's styled line is on screen. A plain SRT rendition is drawn by AVPlayer with no suppression. Media and sidecars come through the authorization relay.swift teston thexcode-27runner (macOS 27) instead ofmacos-15, so the fork's APAC tests, which return early below OS 26, ran for the first time there. The APAC click test calledAVAssetReader.copyNextSampleBuffer()from the cooperative pool, holding one of a three-core runner's threads while the decode waited. It now decodes on a thread of its own (dc4d5433).Evidence: https://evidence.siloserver.org/r/aetherengine/pr-16/
Checklist
CHANGELOG.mdupdatedfeat(...),fix(...),chore(...))Open fork PRs: #12 needs a rebase onto upstream's
AVIOReader(+584/−106); #14 should be retested on this branch, since AE#616 measures off the same renditions; #15 may be superseded by upstream AE#659 and AE#697 (forward-only URL sources now route native) and needs a retest.🤖 Generated with Claude Code
Note
Sync upstream AetherEngine 7.28.3 into Silo fork
Brings the Silo fork up to AetherEngine 7.28.3. The largest changes:
ByteFIFO/SoftwareStoredPacketon-disk formats changed,HLSOriginRelayseals origin references per-instance so pre-existing plaintext-query references are refused, andLiveTelemetrySamplernow reports playhead-crossed bitrate instead of transfer bytes.Macroscope summarized 31047b0.