Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
350 commits
Select commit Hold shift + click to select a range
a101c8e
fix(watch): re-arm the video download gate when the tile reconnects
fperex Sep 17, 2026
54ac2ac
feat(watch): hold the sound for a picture that arrives later
fperex Sep 17, 2026
dd9edf8
fix(watch): cap the cross-track hold at what lip sync is worth
fperex Sep 17, 2026
e98b696
fix(tokio): keep a client alive across a relay restart
fperex Sep 17, 2026
6d7c7ef
fix(ffi): follow the native reconnect give-up default
fperex Sep 17, 2026
f2bb9c5
docs: the resilience matrix, findings 26 to 31 and the reconnect defa…
fperex Sep 17, 2026
b0e12da
test(tokio): measure the outage, not the paused clock's jumps
fperex Sep 17, 2026
3f37e76
docs: the quiet confirmation rows and the gates at the tip
fperex Sep 17, 2026
01e2b5d
docs: the listening round at this tip
fperex Sep 17, 2026
4e6d70e
chore: drop exports nothing consumes and one redundant ring test
fperex Sep 17, 2026
eb0ace2
docs: correct comments a later fix made false
fperex Sep 17, 2026
55aa600
docs: the cleanup sweep and the final counts
fperex Sep 18, 2026
31d1054
fix(watch): quiet the first seconds of a self-publish
fperex Sep 18, 2026
ee59779
test(publish): several watchers joining and leaving audio and video
fperex Sep 18, 2026
ea5d3f9
fix(watch): start playout on the level the ring holds
fperex Sep 18, 2026
e05b5b3
fix(audio): start playout on the level the buffer holds
fperex Sep 18, 2026
7d70c51
fix(watch): keep a rendition's arrival estimate across a gap in the r…
fperex Sep 18, 2026
6f8ab61
fix(watch): hold a departed track's reading in the shared delay for a…
fperex Sep 18, 2026
2ecb1d6
docs: the rebase onto upstream dev and findings 32 to 35
fperex Sep 18, 2026
97ceb46
fix(hang,watch,publish): reopen the timeline after a declared endpoint
fperex Sep 18, 2026
4468127
fix(watch): tune-in and reload without a conviction
fperex Sep 18, 2026
03be5b6
test(shaper): step lists and a queue cap
fperex Sep 18, 2026
e4217c9
fix(packaging): the relay takes its config path positionally
fperex Sep 18, 2026
4bb02b7
test(audio-quality): ceilings for the declared-pause recording
fperex Sep 18, 2026
b22a755
docs: the first ten seconds, findings 36 to 44
fperex Sep 18, 2026
7d977bb
docs: the rebase onto upstream dev, findings 45 to 47
fperex Sep 18, 2026
2716a18
fix(hang): a throttled timer is not a blocked event loop
fperex Sep 19, 2026
a925e16
fix(watch): reach a deeper target by stretching, never by parking
fperex Sep 19, 2026
82071cb
fix(publish,watch): an unmute resumes on the same subscription
fperex Sep 19, 2026
37463cf
fix(audio): expand toward a deeper hold from any level, as the browse…
fperex Sep 19, 2026
ba83dc2
fix(watch): a detached element releases its audio context
fperex Sep 19, 2026
e1d6e0a
fix(test): the native smoke client reads the announce kind
fperex Sep 19, 2026
ba303c5
fix: adapt to upstream's unified watch/publish and catalog APIs
fperex Sep 21, 2026
fb77ba2
fix(hang): measure a slow group against the next group that has a frame
fperex Sep 21, 2026
a6d5301
test(mux): pin that a frameless successor does not hide a stamped group
fperex Sep 21, 2026
55da4b4
fix(net): observe writes before expiring their group
fperex Sep 21, 2026
6e91d86
fix(publish): retain advertised jitter across encoder changes
fperex Sep 21, 2026
1b87084
chore: add a shared upstream sync command
fperex Sep 21, 2026
9de9afb
chore: sync upstream main through d2f881bbb
fperex Sep 21, 2026
b5cbaa4
test(publish): use branded catalog jitter expectations
fperex Sep 21, 2026
b947adf
style: format shared sync scripts
fperex Sep 21, 2026
e9fded9
chore: sync upstream path-role refactor
fperex Sep 21, 2026
d75eacd
test(publish): follow renamed announcement prefix
fperex Sep 21, 2026
a5c95b9
fix(playout): retire stale congestion from the jitter estimate
fperex Sep 21, 2026
992e2f2
fix(publish): preserve audio timestamps across input gaps
fperex Sep 21, 2026
5a708ad
fix(net): maintain expiry and priority until stream acknowledgement
fperex Sep 21, 2026
d833920
fix(watch): cancel worklet loads before replacing their audio context
fperex Sep 21, 2026
06940d0
fix(watch): release overdue video groups against the audio playhead
fperex Sep 21, 2026
98f72ad
fix(watch): report rendered frame rate in player stats
fperex Sep 21, 2026
15958cc
fix(demo): offer a lower video rendition for automatic quality
fperex Sep 21, 2026
09dcd26
fix(watch): pace video against the audio clock rate
fperex Sep 21, 2026
038a6f6
fix(watch): keep late backlog behind the first visible frame
fperex Sep 21, 2026
bc95d26
fix(watch): detect audio gaps during decoder startup
fperex Sep 21, 2026
9fb92ff
fix(watch): keep video presentation on the display refresh
fperex Sep 21, 2026
802561d
fix(watch): pace video from the audio output clock
fperex Sep 21, 2026
8b33afd
fix(relay): wait for HTTP responses in embedding tests
fperex Sep 21, 2026
2d939ad
fix(watch): smooth noise in audio output timestamps
fperex Sep 21, 2026
3083216
fix(watch): retain adjacent frames until presentation
fperex Sep 21, 2026
9a8e842
fix(demo): retain upstream rendition defaults
fperex Sep 22, 2026
e77a962
chore: sync upstream main through 953b79ebc
fperex Sep 22, 2026
91b4396
fix(publish): keep capturing video while the Firefox window is hidden
fperex Sep 22, 2026
f02bf76
fix(audio): migrate merged tests to decode::Options
fperex Sep 22, 2026
f5506ad
fix(watch): keep the playout timeline across a ring resize
fperex Sep 22, 2026
d4fc00b
chore: carry PR #4 through f5506ad9d onto the 953b79ebc sync
fperex Sep 23, 2026
6cd925b
chore: sync upstream main through 7ee2b0241
fperex Sep 23, 2026
6c3a761
chore: regenerate bun.lock after the upstream sync
fperex Sep 23, 2026
df9fab9
docs(watch): fix the audio decoder example and document conceal
fperex Sep 23, 2026
5cd8b89
fix(dart): read moq-ffi from cargo's target directory
fperex Sep 23, 2026
2961b1f
chore(lint): read stereo frames with as_chunks in the engine tests
fperex Sep 23, 2026
33c827c
test(tokio): let a draining peer's budget run out only between attempts
fperex Sep 23, 2026
ceb21e9
test(audio-quality): re-record the plain ring after it keeps its time…
fperex Sep 23, 2026
c1f899e
fix(hang): give up a missing group once the lone group after it spans…
fperex Sep 23, 2026
91d2150
feat(playout): raise the target after the player runs dry
fperex Sep 23, 2026
c0bbc90
fix(playout): learn only from run-dries the page itself caused
fperex Sep 23, 2026
98f475c
perf(watch): stop allocating on the audio thread
fperex Sep 23, 2026
b92e6e1
fix(hang): log a covered-group skip at debug
fperex Sep 23, 2026
f736d9a
test(audio-quality): re-record mic-firefox after the run-dry safety net
fperex Sep 23, 2026
7293200
ci(audio-quality): run the replay lane on player PRs and the matrix o…
fperex Sep 23, 2026
f777ea1
test(watch): hold the stretch bench to its budget, not a ratio
fperex Sep 23, 2026
063a14d
test(audio-quality): re-record mic-remote plain's stretch ceilings
fperex Sep 23, 2026
0bd542a
docs: close the covered-group warning item in the handoff
fperex Sep 23, 2026
12f9514
chore: sync upstream main through ffa5b81b3
fperex Sep 24, 2026
d342c3d
chore: regenerate bun.lock after the upstream sync
fperex Sep 24, 2026
7655be2
fix(publish): warn rather than error on a refused capture
fperex Sep 24, 2026
f56df2e
Revert "test(audio-quality): re-record mic-firefox after the run-dry …
fperex Sep 24, 2026
6797fc2
Revert "fix(playout): learn only from run-dries the page itself caused"
fperex Sep 24, 2026
b722f99
Revert "feat(playout): raise the target after the player runs dry"
fperex Sep 24, 2026
5928fe3
Revert "fix(hang): give up a missing group once the lone group after …
fperex Sep 24, 2026
4c7a652
Merge pull request #4 from fperex/codex/pr3-expired-writes
fperex Sep 24, 2026
2717154
feat(watch): let the render worklet take ring writes from a second port
fperex Sep 24, 2026
0a5da40
fix(net): accept a session's bidi streams through one reader
fperex Sep 24, 2026
5611277
refactor(watch): move the audio supply out of the decoder
fperex Sep 24, 2026
7aeb42c
feat(watch): let Sync.received take the time a frame arrived
fperex Sep 24, 2026
87d01e4
docs(watch): point comments at the supply's methods
fperex Sep 24, 2026
971c741
feat(watch): add the audio worker's protocol and the page's pool
fperex Sep 24, 2026
464e8dd
feat(watch): run each player's audio supply in the worker
fperex Sep 24, 2026
e45f6ca
docs(watch): point two more comments at the latency walk
fperex Sep 24, 2026
86a88a0
feat(watch): let the audio decoder feed its ring from the page's worker
fperex Sep 24, 2026
5c88f1f
feat(watch): offload audio by default, and take it back when the work…
fperex Sep 24, 2026
5dbb5e3
test(watch): run the audio host's tests on fake timers
fperex Sep 24, 2026
666cb5c
fix(watch): keep audio on the page where nothing can play it
fperex Sep 24, 2026
b1f593d
feat(watch): keep a player's audio on the page with offload="false"
fperex Sep 24, 2026
47ee569
docs(concept): say which thread maps the audio playhead's clock
fperex Sep 24, 2026
c8736df
test(interop): require the player's audio on the thread each media ru…
fperex Sep 24, 2026
4fb638e
test(audio-quality): record which thread fed each row's audio, and vo…
fperex Sep 24, 2026
98c6668
feat(demo): add bench params to the watch page: offload, stall and csp
fperex Sep 24, 2026
76c83b4
fix(audio-quality): start the harness relay on the renamed [listen] s…
fperex Sep 24, 2026
a07f3c7
test(audio-quality): run the Chromium lane with the audio on the main…
fperex Sep 24, 2026
b8746c4
fix(watch): write the audio worker's ring by message on every page
fperex Sep 25, 2026
c24fbb5
fix(watch): say when the render worklet cannot read a message
fperex Sep 25, 2026
d8a3e89
fix(watch): name the worklet when it never reads the worker's ring
fperex Sep 25, 2026
d633fc1
chore: sync upstream main through 75d615dfd
fperex Sep 25, 2026
bbc2822
feat(shaper): pipe TCP on the shaper's port to the target, as an option
fperex Sep 25, 2026
8ace4b8
feat(shaper): a gaussian jitter model that keeps the order, as an option
fperex Sep 25, 2026
2a0432d
feat(shaper): one link each way for every client, as an option
fperex Sep 25, 2026
f684aa6
feat(shaper): batches that release datagrams together, as an option
fperex Sep 25, 2026
34c6ce0
feat(shaper): steps that change a profile part-way through a run
fperex Sep 25, 2026
da0c96f
feat(shaper): named profiles for both directions, loaded from TOML
fperex Sep 25, 2026
7c09168
feat(shaper): a JSON report of the counters, as an option
fperex Sep 25, 2026
5cc94fb
fix(shaper): send a datagram that is already due at once, not on a timer
fperex Sep 25, 2026
6485da9
docs(shaper): a README for the shaper's usage and its options
fperex Sep 25, 2026
1217ea7
test(audio-quality): run the Chromium lane on the synced shaper
fperex Sep 25, 2026
1ee7824
fix(ci): include audio quality in JavaScript gates
fperex Sep 28, 2026
1dfa197
test(audio-quality): prove output and sampling safeguards
fperex Sep 28, 2026
e8e61e7
fix(shaper): verify traffic under each active profile
fperex Sep 28, 2026
227faa4
fix(audio): preserve the noise estimate through digital silence
fperex Sep 28, 2026
0667ad6
fix(audio): drain partial runs and park declared pauses
fperex Sep 28, 2026
6e41970
fix(mux): avoid registering a reader for skip diagnostics
fperex Sep 28, 2026
f47fcee
fix(relay): refuse new io_uring sessions during drain
fperex Sep 28, 2026
340f3cd
fix(watch): preserve noise estimate across digital silence
fperex Sep 28, 2026
09941b9
fix(watch): terminate replaced audio worklet processors
fperex Sep 28, 2026
66c3930
fix(watch): retain decoded sample rate during worker fallback
fperex Sep 28, 2026
ee9d7cd
fix(watch): refuse workers that never finish starting
fperex Sep 28, 2026
f8ee37a
fix(watch): wait for timing before building worker rings
fperex Sep 28, 2026
2e8bd11
fix(watch): keep worker ring reports off the page thread
fperex Sep 28, 2026
2c607c9
fix(watch): avoid waking video for submillisecond clock noise
fperex Sep 28, 2026
4c5a1bf
fix(watch): map playback clocks only through valid device time
fperex Sep 28, 2026
6a6ee9d
test(watch): run decoder recovery against virtual time
fperex Sep 28, 2026
10d8751
fix(watch): pace rendition promotion and learn sparse video cadence
fperex Sep 28, 2026
ad40bf8
fix(watch): present frame pairs without accumulating refresh lag
fperex Sep 28, 2026
88bba04
test(shaper): bound forwarding benchmark completion
fperex Sep 28, 2026
26f71e4
fix(net): release expiry guards at FIN and use Old resets
fperex Sep 28, 2026
f3e2f8a
fix(hang): bound paused readers and preserve marker events
fperex Sep 28, 2026
ad1380b
fix(publish): accept reordered capture callbacks after worker ticks
fperex Sep 28, 2026
3f1b754
style(watch): format regression fixes
fperex Sep 28, 2026
d209e30
fix(watch): retain decoded rates only for matching configurations
fperex Sep 28, 2026
c6c842c
fix(net): hold a parked track's warm cache until the upstream confirm…
kixelated Sep 25, 2026
2fe47e4
fix(net): judge spliced staleness against the logical live edge (#4103)
kixelated Sep 25, 2026
294ee03
fix(net): skip a stale warm cache on an IETF rejoin (#4150)
kixelated Sep 25, 2026
b3da592
fix(cli): close the relay connection on SIGINT and SIGTERM (#4287)
kixelated Sep 27, 2026
722f22a
fix(cli): handle SIGTERM during native playback
fperex Sep 28, 2026
9ed7494
fix(audio): preserve media time while draining across holes
fperex Sep 28, 2026
faea8b9
refactor(watch): trim playback code and cover 100 viewers
fperex Sep 28, 2026
6156401
docs(watch): remove stale playback comments
fperex Sep 28, 2026
aa816c7
chore: remove stale fork investigation notes and comments
fperex Sep 28, 2026
057e01b
fix(watch): exclude isolated outages from video cadence
fperex Sep 28, 2026
d836924
perf(audio): remove allocation and backlog scans from playout
fperex Sep 28, 2026
f0dc165
ci: measure native audio playout nightly
fperex Sep 28, 2026
55daa91
style: finish scope guard and test cleanup
fperex Sep 28, 2026
a7dd859
style(audio): keep benchmark manifest tables sorted
fperex Sep 28, 2026
7ab0463
perf(net): skip redundant anchors for an unbounded resumed track
fperex Sep 28, 2026
1071688
test(relay): isolate process-wide shutdown signals
fperex Sep 28, 2026
97d712e
fix(watch): keep stale GOPs out of video decode
fperex Sep 28, 2026
14bd9dd
fix(hang): keep historical loss behind the playhead
fperex Sep 28, 2026
3db9ba7
perf(net): skip drained resume anchor work
fperex Sep 28, 2026
84e98b3
test(net): track live fanout cost in nightly
fperex Sep 28, 2026
33374f1
fix(publish): disconnect each capture edge once
fperex Sep 29, 2026
d23787a
fix(publish): stop retired capture worklets
fperex Sep 29, 2026
ac22867
test(tokio): cover open warm edge rejoin
fperex Sep 29, 2026
57bf824
test(publish): await capture messages without sleeps
fperex Sep 29, 2026
fbec46a
test(tokio): release stale edge ownership directly
fperex Sep 29, 2026
5545dfd
fix(bench): send QUIC closes before exiting
fperex Sep 29, 2026
b0578b0
fix(audio): count playback skips at the ring
fperex Sep 29, 2026
6671646
fix(audio-quality): clear failed analyser replacements
fperex Sep 29, 2026
0251594
test(audio): attest the concrete ring in browser quality runs
fperex Sep 29, 2026
74aaab8
refactor(audio): limit shared ring resizing to growth
fperex Sep 29, 2026
10a1a5b
docs(audio-quality): distinguish isolation from ring transport
fperex Sep 29, 2026
8b2fcea
fix(audio-quality): measure signed stretch counters consistently
fperex Sep 29, 2026
2f010a0
test(browser): release console argument handles
fperex Sep 29, 2026
818c9ae
fix(audio): observe playback jumps at reader commit
fperex Sep 29, 2026
75280d4
fix(watch): keep pause clickable while buffering
fperex Sep 29, 2026
59c6a23
fix(net): expire takeover tails from the serving route
fperex Sep 29, 2026
c7faf59
fix(audio): count only concurrent overflow discards
fperex Sep 29, 2026
f212825
fix(net): wake takeover tails as the live edge advances
fperex Sep 29, 2026
ca48174
fix(cli): drain stdin in pipe-sized reads
fperex Sep 29, 2026
6b25efc
fix(watch): close the audio context once its render processor has sto…
fperex Sep 29, 2026
78923c1
fix(publish): close the capture context once its processor has stopped
fperex Sep 29, 2026
7f02213
test(watch): a reopened track must not step the picture back
fperex Sep 29, 2026
4844bc2
fix(watch): never step a reopened track's picture back
fperex Sep 29, 2026
2e6f92c
test(relay): a returning viewer is never served media from before it …
fperex Sep 29, 2026
bdec360
test(audio-quality): pass a silence breach only when every quiet wind…
fperex Sep 29, 2026
395ec81
test(audio-quality): give the quiet-proof fixture transients and cove…
fperex Sep 29, 2026
c3996e3
test(audio-quality): bracket quiet windows per report interval and fi…
fperex Sep 29, 2026
1f9e5e1
test(audio-quality): prove each analyser-lag segment of a row on its own
fperex Sep 29, 2026
84fac1c
fix(audio-quality): keep the publishers at real time after a stall
fperex Sep 29, 2026
7ede921
test(relay): a returning lite viewer of a paused publisher gets no st…
fperex Sep 29, 2026
9300273
feat(net): let a publisher mark a break where new subscriptions start
fperex Sep 29, 2026
38f75ef
fix(hang,publish): declare the break at every pause marker
fperex Sep 29, 2026
738f9f1
fix(watch): keep the held picture until a reopened track paints
fperex Sep 29, 2026
36bcd4b
test(audio): stop fake worklet processors after close as an engine does
fperex Sep 29, 2026
a17fe6d
fix(mux): declare the break at every discontinuity marker
fperex Sep 29, 2026
6d179bf
refactor(watch): one decode loop for legacy and CMAF video
fperex Sep 29, 2026
15e2e11
refactor(publish): one capture attempt for camera and microphone
fperex Sep 29, 2026
2e68919
test(audio-quality): drop the two metrics nothing measures
fperex Sep 29, 2026
b4addef
ci: nightly calls the audio quality workflow instead of a copy of it
fperex Sep 29, 2026
1064fe2
docs(watch,publish): cut history and repeats from comments
fperex Sep 29, 2026
5f2142a
test(audio-quality): keep the harness provenance in one place
fperex Sep 29, 2026
3534dd3
refactor(mux): drop the diagnostic fields from the slow-group log
fperex Sep 29, 2026
bf5407b
docs(rs): cut history and repeats from audio, cli, relay and tokio co…
fperex Sep 29, 2026
ee65505
refactor(watch): drop Sync's delay effect, which only copied the jitter
fperex Sep 29, 2026
1d45058
refactor(hang): drop the diagnostic fields from the slow-group log
fperex Sep 29, 2026
e62a519
test(hang): stub the stall monitor instead of spinning the CPU
fperex Sep 29, 2026
809faac
docs(watch): keep one copy of the ring transports' shared docs
fperex Sep 29, 2026
58150f5
docs(concept): drop the playout doc's history and fix the renderer claim
fperex Sep 29, 2026
63f82df
test(watch): count report and stretch work instead of timing it
fperex Sep 29, 2026
1203d1f
test(watch,hang): share the duplicated test fakes
fperex Sep 29, 2026
3939288
test(watch): run the rings' shared cases once over both transports
fperex Sep 29, 2026
ecddeb5
refactor(hang,publish): one writer for the legacy endpoint marker
fperex Sep 29, 2026
ee66ffa
perf(watch): no typed-array views on the audio thread in stretch, mer…
fperex Sep 29, 2026
b8a8aae
docs(watch,hang): cut comments that restate the playout doc or narrat…
fperex Sep 29, 2026
a10efb9
ci: run the audio worklet and report benchmarks nightly
fperex Sep 29, 2026
9eb2170
docs(watch): point the report benchmark section at the opt-in script
fperex Sep 29, 2026
e9ce90f
build(js): keep test support and benchmarks out of the published pack…
fperex Sep 29, 2026
ba715a5
perf(net): keep a resumed track's own groups off the anchor refresh
fperex Sep 29, 2026
2e67062
perf(net): shrink the relay's per-subscriber splice state
fperex Sep 29, 2026
a461694
perf(net): skip the drift anchor lock on an unanchored cursor
fperex Sep 29, 2026
492d0d2
perf(net): resolve a plain cursor's drift only for a group it judges
fperex Sep 29, 2026
634f9ab
perf(net): read a splice's live edge from the newest segment down
fperex Sep 29, 2026
769f488
perf(net): skip re-pushing an unchanged anchor onto a segment cursor
fperex Sep 29, 2026
d972fbd
test(net): pin delivery's revalidation of a pushed edge
fperex Sep 29, 2026
b38f8f4
perf(net): pick the drift edge with a match instead of an iterator chain
fperex Sep 29, 2026
2c34a8b
fix(watch): let a declared break re-open video GOP order
fperex Sep 29, 2026
626cfa2
ci(audio-quality): keep nightly and manual runs out of each other's g…
fperex Sep 29, 2026
0df56d5
perf(watch): stop allocating the shown picture record per painted frame
fperex Sep 29, 2026
57bdcc8
docs(watch): state the audio worklet CSP requirement
fperex Oct 1, 2026
ba8cc67
fix(hang): preserve burst cadence when timers are rationed
fperex Oct 1, 2026
6656034
docs: explain receiver gap heuristic limits
fperex Oct 1, 2026
bc26802
fix(test): pace the fixed audio quality control
fperex Oct 1, 2026
a3f1a74
fix(net): keep finished warm confirmations reusable by other fronts
fperex Oct 1, 2026
75584b8
fix(deps): replace the yanked yoke-derive lock entry
fperex Oct 1, 2026
df3d2b4
test(signals): measure retention of owned spawn tasks
fperex Oct 1, 2026
2a78f74
test(relay): advance auth expiry outside network waits
fperex Oct 1, 2026
ab075bb
test(relay): keep media active between auth clock steps
fperex Oct 1, 2026
a99d83d
test: retain audio quality traces after page close
fperex Oct 1, 2026
ac6461a
test: mock time for catalog completion selection
fperex Oct 1, 2026
19bcd9d
test: identify retained worker sockets by owned inode
fperex Oct 1, 2026
09a783a
test: report browser cleanup failures in media checks
fperex Oct 1, 2026
9b2d051
Merge pull request #57 from fperex/codex/fork-only-unslop
fperex Oct 1, 2026
c11d5cb
ci: bind Rust target caches to Nix inputs
fperex Oct 1, 2026
d5937f5
test(auth): await session end requests
fperex Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
53 changes: 53 additions & 0 deletions .agents/commands/sync-upstream
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ ${1:-} == --help ]]; then
echo 'Usage: bash .agents/commands/sync-upstream [remote [branch]]'
echo 'Defaults: upstream main. Fetch, back up HEAD, and prepare a merge for review.'
echo 'Requires a clean branch. Never stashes, commits, rebases, or pushes.'
exit 0
fi
if (($# > 2)) || [[ ${1:-} == -* || ${2:-} == -* ]]; then
echo 'Expected a remote name and branch name. Use --help.' >&2
exit 2
fi
remote=${1:-upstream}
branch=${2:-main}
cd "$(git rev-parse --show-toplevel)"
git symbolic-ref --quiet HEAD >/dev/null || {
echo 'Refusing to sync a detached HEAD. Select the intended working branch.' >&2
exit 1
}
for state in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-merge rebase-apply; do
if [[ -e $(git rev-parse --git-path "$state") ]]; then
echo "Finish or abort the existing Git operation first: $state" >&2
exit 1
fi
done
if [[ -n $(git status --porcelain --untracked-files=normal) ]]; then
echo 'Commit the intended work first. The sync command does not stash or discard changes.' >&2
exit 1
fi
git remote get-url "$remote" >/dev/null
git check-ref-format "refs/heads/$branch"
git fetch --no-tags "$remote" "refs/heads/$branch"
target=$(git rev-parse FETCH_HEAD)
git merge-base HEAD "$target" >/dev/null || {
echo 'Refusing to merge unrelated histories.' >&2
exit 1
}
if git merge-base --is-ancestor "$target" HEAD; then
echo "Already contains $remote/$branch at $target."
exit 0
fi
backup="refs/backups/sync/$(date -u +%Y%m%dT%H%M%SZ)-$$"
git update-ref "$backup" HEAD ''
echo "Backup: $backup"
echo "Incoming commits from $remote/$branch:"
git log --oneline "HEAD..$target"
if ! git -c merge.autoStash=false merge --no-commit --no-ff "$target"; then
echo 'Merge stopped. Resolve and commit, or run git merge --abort to return to the backup.' >&2
exit 1
fi
echo 'Review the staged merge, validate affected packages, and commit with your own attribution.'
echo 'No remote branch has been changed.'
74 changes: 74 additions & 0 deletions .agents/commands/sync-upstream.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail

command_path="$(cd "$(dirname "$0")" && pwd)/sync-upstream"
fixture=$(mktemp -d)
trap 'rm -rf "$fixture"' EXIT
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
export GIT_AUTHOR_NAME='Sync test' GIT_AUTHOR_EMAIL='sync@example.invalid'
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
git init -q -b main "$fixture/upstream"
cd "$fixture/upstream"
echo initial >shared
git add shared
git commit -qm initial
git clone -q . "$fixture/work"
cd "$fixture/work"
git remote rename origin upstream
git switch -qc fork
echo fork >local
git add local
git commit -qm fork
before=$(git rev-parse HEAD)
cd "$fixture/upstream"
echo incoming >incoming
git add incoming
git commit -qm incoming
incoming=$(git rev-parse HEAD)
cd "$fixture/work"

echo dirty >untracked
if bash "$command_path"; then
echo 'Accepted dirty worktree' >&2
exit 1
fi
[[ $(git rev-parse HEAD) == "$before" && -f untracked ]]
rm untracked
bash "$command_path"
[[ $(git rev-parse HEAD) == "$before" ]]
[[ $(git rev-parse MERGE_HEAD) == "$incoming" ]]
[[ $(cat local) == fork && $(cat incoming) == incoming ]]
[[ $(git for-each-ref --format='%(objectname)' refs/backups/sync) == "$before" ]]
if bash "$command_path"; then
echo 'Accepted pending merge' >&2
exit 1
fi
git commit -qm 'merge upstream'
merged=$(git rev-parse HEAD)
bash "$command_path"
[[ $(git rev-parse HEAD) == "$merged" ]]
git merge-base --is-ancestor "$incoming" HEAD

echo fork >shared
git add shared
git commit -qm 'fork conflict'
before=$(git rev-parse HEAD)
cd "$fixture/upstream"
echo upstream >shared
git add shared
git commit -qm 'upstream conflict'
cd "$fixture/work"
if bash "$command_path"; then
echo 'Accepted conflicting merge' >&2
exit 1
fi
[[ -n $(git ls-files -u) ]]
git merge --abort
[[ $(git rev-parse HEAD) == "$before" && $(cat shared) == fork ]]
[[ -z $(git status --porcelain) ]]
git switch -q --detach
if bash "$command_path"; then
echo 'Accepted detached HEAD' >&2
exit 1
fi
echo 'Sync command tests passed.'
20 changes: 20 additions & 0 deletions .claude/skills/sync-upstream/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: sync-upstream
description: Sync the current MoQ working branch with upstream changes and validate the integration. Use for requests to sync or update the fork from upstream.
---

Run from the intended worktree. Inspect its branch, status, remotes, and PR base first. Preserve any work in progress with focused commits before syncing; do not stash, reset, or change another worktree.

The shared command is available to any harness with a shell:

```bash
bash .agents/commands/sync-upstream
```

It defaults to `upstream main`. Supply another configured remote and branch only when the task requires it. The command fetches, records a backup ref, and prepares a merge without committing or pushing. Repeating it after the merge is committed does nothing until upstream advances. A pending merge must be completed or aborted before another sync.

Resolve conflicts by understanding both sides, including the fork's behavioral fixes and upstream API migrations. Review the entire staged diff, run affected regression tests and the repository's `just check` and `just test` in Nix, and report any incomplete checks. Do not mix builds with live media measurements. Keep before/after measurements on the same upstream base.

Commit the reviewed merge with a conventional subject and your own `Co-Authored-By` trailer. Report the old and new upstream commits, conflict resolutions, validation, and public API or wire impact. Only push or update a PR when the current user request authorizes it, using the requested fork and target branch.

This skill is shared through the repository's `.agents/skills` symlink to `.claude/skills`. Claude Code can invoke `/sync-upstream`; other harnesses can load this skill or run the shell command directly.
1 change: 1 addition & 0 deletions .github/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ set fallback
# Lint GitHub Actions workflows.
check:
python3 {{ source_directory() }}/scripts/install-formula.test.py
bun test {{ source_directory() }}/scripts/rust-cache.test.ts
{{ source_directory() }}/scripts/release.test.sh
@if command -v nfpm >/dev/null 2>&1 && command -v envsubst >/dev/null 2>&1 && command -v dpkg-deb >/dev/null 2>&1 && command -v rpm >/dev/null 2>&1; then python3 {{ source_directory() }}/../rs/scripts/package-rename.test.py; fi
@if command -v actionlint >/dev/null 2>&1; then actionlint; fi
Expand Down
65 changes: 65 additions & 0 deletions .github/scripts/rust-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, test } from "bun:test";
import path from "node:path";

type Mapping = { [key: string]: unknown };

const workflows = [
{ file: "interop.yml", job: "interop" },
{ file: "nightly.yml", job: "nightly" },
];

const github = path.resolve(import.meta.dir, "..");

function mapping(value: unknown, label: string): Mapping {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`${label} is not a YAML mapping`);
}

return { ...value };
}

function sequence(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) throw new TypeError(`${label} is not a YAML sequence`);
return value;
}

async function yaml(file: string): Promise<Mapping> {
return mapping(Bun.YAML.parse(await Bun.file(file).text()), file);
}

describe("Rust cache boundary", () => {
for (const workflow of workflows) {
test(`${workflow.file} uses the flake-keyed local cache action`, async () => {
const file = path.join(github, "workflows", workflow.file);
const document = await yaml(file);
const jobs = mapping(document.jobs, `${file}: jobs`);
const job = mapping(jobs[workflow.job], `${file}: jobs.${workflow.job}`);
const steps = sequence(job.steps, `${file}: jobs.${workflow.job}.steps`).map((step, index) =>
mapping(step, `${file}: jobs.${workflow.job}.steps[${index}]`),
);
const cache = steps.find((step) => step.name === "Rust cache" || step.name === "Rust Cache");

expect(
steps.some((step) => typeof step.uses === "string" && step.uses.startsWith("Swatinem/rust-cache@")),
).toBeFalse();
expect(cache?.uses).toBe("./.github/actions/rust-cache");
});
}

test("cache and restore keys bind the runner and pinned Nix toolchain", async () => {
const file = path.join(github, "actions/rust-cache/action.yml");
const document = await yaml(file);
const runs = mapping(document.runs, `${file}: runs`);
const steps = sequence(runs.steps, `${file}: runs.steps`).map((step, index) =>
mapping(step, `${file}: runs.steps[${index}]`),
);
const cache = steps.find(
(step) => typeof step.uses === "string" && step.uses.startsWith("jdx/mr-boxington-action@"),
);
const inputs = mapping(cache?.with, `${file}: cache inputs`);
const prefix = `\${{ runner.os }}-\${{ runner.arch }}-mbx-target-\${{ inputs.scope }}-\${{ hashFiles('flake.nix', 'flake.lock', 'rust-toolchain.toml') }}-`;

expect(inputs["restore-keys"]).toBe(prefix);
expect(typeof inputs["cache-key"] === "string" && inputs["cache-key"].startsWith(prefix)).toBeTrue();
});
});
5 changes: 5 additions & 0 deletions .github/workflows/alert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ on:
# Rust cache on `main`, and that push run is nobody's check, so a break in
# it would otherwise go unseen. The cost is a skipped Alert run per Swift
# pull request.
#
# Audio quality is here for its manual matrix run, which is not a pull
# request. The cost is a skipped Alert run per pull request its replay lane
# runs on.
workflows:
- apt-repo
- Audio quality
- Cache
- Cachix
- Docker
Expand Down
Loading
Loading