Derive přegeneruje každý commitnutý odvozený artefakt a každá relace jej potřebuje, než může commitovat. Na nečinném stroji trvá asi deset minut a na sdíleném MacBooku pod zátěží až hodinu. majordomus offload spustí derive větve na jiném stroji, který repozitář deklaruje, v řadě onoho stroje, a odvozené soubory přinese zpět připravené (staged). Nic se necommituje ani nepushuje. Chování tak, jak je implementováno a testováno. Kde se tato stránka a spustitelný soubor rozcházejí, je chybná stránka a mění se ve stejném commitu.
Modul je apps/majordomus-cli/src/offload/. Pravidlo je project.a-derive-offload-waits-its-turn a úkol, který ho vytvořil, je I2398. Před ním byl offload skriptem v domovském adresáři s napsanými hosty. Jeho zámek byl until mkdir lock; do sleep 15; done. 2026-10-11 v 00:18Z takto čekalo na lundra pět relací, nejstarší 2 h 12 min, zatímco zámek vyhrávali pozdější příchozí.
Příkazy
majordomus offload hosts # the declaration, defaults filled in; contacts nobody
majordomus offload probe # measure every host, name the one a derive would take
majordomus offload derive . # derive this worktree on the chosen host, stage the result
majordomus offload derive . --host lundra # on a named host (a declared id or address)
majordomus offload derive . --keep # keep the host's worktree afterwards
--declaration <file> čte soubor deklarace místo repozitářového a --format json vypíše jeden dokument. Spustitelné příklady jsou v generované referenci (generated/cli.md, pod majordomus offload).
| ukončení | offload derive |
|---|---|
| 0 | odvozeno; změněné soubory jsou připraveny ke commitu, nebo derive nic nezměnil |
| 2 | nesledovaný soubor, nevyřešený merge nebo merge s neindexovanými změnami: nic se nepřeneslo |
| 10 | derive na hostiteli selhal; posledních několik řádků jeho logu se vypíše a jeho worktree tam zůstane |
| 11 | derive smazal sledovaný soubor; nic nebylo aplikováno a snímek je pojmenován pro ruční aplikaci |
| 12 | žádný deklarovaný hostitel jej nemůže převzít, hostitele nebylo možné použít, nebo se výsledek neaplikoval |
| 75 | deklarované čekání fronty vypršelo; lístek je stažen |
offload probe skončí s 12, když není způsobilý žádný hostitel. offload hosts skončí s 0, i když není nic deklarováno: prázdná deklarace je odpověď.
Deklarace
Hostitelé a jejich limity jsou zapsáni jednou v .ai/repo/offload/derive.yaml. Druh je derive-offload a schéma derive-offload/v1 (share/schemas/majordomus/derive-offload/). Každé sestavení indexu je validuje a obsluhuje se jako jakýkoli jiný objekt vrstvy, jako majordomus://derive-offload/derive. Žádný skript ani řádek crate nenese adresu hostitele.
| pole | platí | výchozí |
|---|---|---|
hosts[] | id, address (user@host), volitelné identity_file, min_free_gib, description; pořadí rozhoduje o shodě | — (jedno je povinné) |
min_free_gib | GiB, které musí být volné pod domovským adresářem hostitele, aby byl vybrán | 10 |
base | ref, proti kterému se balík řeže | origin/master |
queue.directory, queue.lock | adresář ticketů a zámek, absolutní na hostiteli | /tmp/offload-queue, /tmp/offload-derive.lock |
queue.poll_seconds, queue.wait_max_seconds | mezi pohledy a celkem | 5, 14400 |
remote.checkout, remote.worktrees | checkout hosta a kde se vytváří worktree každého derive, pod jeho home | dev/prismatic-majordomus, dev/prismatic-majordomus-wt |
remote.target | sdílený adresář sestavení, z něhož si každé derive klonuje vlastní | .cache/majordomus-offload-target |
remote.spool | kam se zapisují balíčky a log derive | /tmp |
remote.path, remote.derive, remote.touch | předpona PATH, příkaz derive a soubory dotčené před ním | adresáře bin z asdf, cargo a Homebrew; scripts/derive; kořeny crate |
Přidání stroje je jedna položka pod hosts:. Stroj potřebuje přístup ssh bez promptu, checkout tohoto repozitáře v remote.checkout, z jehož origin umí fetchovat, a toolchain, s nímž scripts/derive staví. offload probe je kontrola, že odpovídá.
Výběr hostitele
Každý deklarovaný hostitel se jednou přes ssh zeptá na volné místo pod svým domovem a na svou frontu. Hostitel se přeskočí a sonda řekne proč, když:
- neodpovídá, nebo odpovídá něčím, co fronta nečte, například přihlašovacím bannerem;
- má méně volného místa, než je jeho minimum;
- nehlásí žádnou hodnotu volného místa, protože neznámé nikdy nestačí.
Z hostitelů, kteří zbyli, derive vezme toho s nejmenším počtem běžících nebo čekajících derive: každý živý tiket, plus jedna, když je zámek držen. Při shodě vyhrává hostitel deklarovaný první.
Fronta
Každý hostitel provádí najednou jedno offloadnuté odvození a jeho čekatelé se obsluhují v pořadí, v jakém přišli. Rozhodnutí se činí v Rustu nad tím, co hostitel hlásí (offload/queue.rs). Hostitel spouští krátké skripty, které jen pozorují a jednají. Každou hodnotu přebírají jako argument, nikdy jako součást svého textu.
- Čekající spustí na hostiteli držitele, proces, který žije přesně tak dlouho jako spojení čekajícího. Poté zapíše lístek pojmenovaný
<arrival>-<holder pid>-<tag>, kde příchod je devatenáct číslic nanosekund od epochy. - Čekatel získá zámek, jen když je jeho lístek nejstarší z těch, jejichž držitel žije. Zámek je adresář vytvořený
mkdira drží jeden prázdný soubor na proces derive, který jej drží, pojmenovaný jeho pid. - Lístek, jehož držitel je mrtvý, odstraní ten, kdo ho uvidí. Čekající, který byl zabit nebo ztratil spojení, nikoho neblokuje.
- Zámek, jehož všichni držitelé jsou mrtví, získá zpět kterýkoli čekající a získání jmenuje mrtvé pidy.
- Adresář zámku bez souboru pid vzal čekatel starých skriptů a nikdo to nemůže ověřit. Čeká se na něj a nikdy se nepřebírá.
Cesty a názvy ticketů jsou ty, které používalo ~/dev/.derive-offload-v2.sh. Čekatel v2 a čekatel tohoto příkazu proto vidí vzájemně své tickety, zatímco se relace přesouvají. Čekatel v1 o zámek stále soupeří, dokud jeho relace nepřestane zámek používat.
Cesta tam a zpět
- Co putuje. Worktree tak, jak stojí: HEAD, když je čisté, jinak snímek
git stash createjeho sledovaných a připravených změn. Ten zapíše objekt commitu a nedotkne se indexu, worktree ani hooku. Commit hook odmítne strom, který ještě není odvozen, takže změnu nelze commitnout před jejím derive. Nesledované soubory by nepřenesly a jsou odmítnuty. - Probíhající merge putuje jako skutečný merge commit s oběma rodiči, zapsaný
git commit-treez připraveného stromu, jakmile je každý konflikt vyřešen. Stash by druhého rodiče zahodil a changelog, který se skládá z logu, by se odvodil bez mergnutých commitů (PR #854, 2026-10-10). Odvozené soubory se vracejí připravené do merge agit commitjej dokončí přes hook. - Svazek (bundle) obsahuje jen commity, kterých
basenedosáhne, a jde přes standardní vstup ssh. Vstup, kteréhobaseuž dosáhne, se do svazku nedává, protože ho hostitel má. Nic se nepushuje na origin, takže nezačne žádný běh CI. - Na hostiteli derive běží v odpojeném worktree
offload-<tag>, ve své řadě. Staví v build adresáři vlastním: klonu sdíleného adresáře typu copy-on-write, kde souborový systém klony má (cp -cna APFS,cp --reflink=alwaysna Linuxu), jinak v prázdném. Adresář se po skončení derive odstraní. Build do sdíleného adresáře derivem, který zámek nikdy nevzal, tak nemůže nahradit spustitelný soubor tohoto derive. Na druhém MacBooku 2026-10-11 v 00:25Z to udělal a stage C odmítla s „this executable is not of this tree's generation“. - Zpět zde se soubory, které derive změnil, vyhodí (checkout) z odvozeného snímku a připraví (stage) a worktree se s tímto snímkem porovná. Derive, který maže sledovaný soubor, je odmítnut dříve, než se cokoli aplikuje. Balíky se z hostitele odstraní, stejně jako jeho worktree, pokud není zadáno
--keepnebo derive neselhal.
Vlastní zámek derive stroje (project.one-derive-at-a-time) se neberou: hlídá tento stroj, a tento stroj neodvozuje. Na hostiteli scripts/derive bere zámek stroje tohoto hostitele jako obvykle.
Nástěnka a mesh
Offload je sdílený zdroj a nástěnka peerů a mesh jsou způsob, jak ho relace sdílejí:
- Před dlouhým derive to uveďte ve svém záměru
majordomus_announce, s hostitelem, pokud jste nějakého určili. Peer, který čtemajordomus_peers, pak ví, proč je fronta na lundra dlouhá. offload probeje měření. Čtěte je místo hádání, který hostitel je volný, a zámek nikdy nebertě ručně: ručně vytvořený prázdný adresář zámku je ten, na který musí fronta čekat navždy.- Relace na jiném stroji odesílá práci na stejné hostitele a vstupuje do stejných front, takže fronta je koordinací napříč stroji. Pro běžný derive není třeba žádný nárok v mesh. Když se práce na hostiteli nesmí napříč stroji překrývat, například údržba, která vyprázdní jeho sdílený adresář buildu, nejdřív si ho vyžádejte výhradně pomocí
majordomus_mesh_claim. - Nikdy nespouštějte
scripts/derivena hostiteli ručně, dokud tam běží offloady. Nebere ani frontu, ani zámek offloadu.
Testy
| soubor | proves |
|---|---|
apps/majordomus-cli/src/offload/queue.rs (jednotka) | rozhodnutí nad pozorováními: pořadí, mrtvé tikety, získaný mrtvý zámek, počkáno na neověřitelný zámek, ztracený tiket, odmítnutá nečitelná odpověď |
apps/majordomus-cli/src/offload/host.rs (jednotka) | volba hostitele: nejméně vytížený, shody podle pořadí deklarace, nedostupný, pod svým minimem, neznámé místo, vlastní minimum hostitele |
apps/majordomus-cli/src/offload/declaration.rs (jednotka) | deklarace: výchozí hodnoty, neznámé klíče odmítnuty, adresy a cesty validovány |
apps/majordomus-cli/tests/offload.rs | fronta proti skutečnému adresáři se skutečnými procesy držitelů a příkaz od začátku do konce proti hostitelům, kterými jsou adresáře na tomto stroji, dosažené přes lokální náhradu za ssh |
Testy od začátku do konce nepotřebují síť. MAJORDOMUS_OFFLOAD_SSH pojmenovává náhradu, která spouští vzdálený příkaz pod domovským adresářem falešného hostitele. Volné místo hostitele je to, co hlásí jeho náhrada df, a hostitel, který je nedostupný, odmítne spojení s kódem 255 ssh. Dokládají tato chování:
- dva čekající jsou obslouženi v pořadí příchodu, ačkoli později příchozí začal čekat dřív;
- lístek mrtvého čekatele frontu neblokuje;
- hostitel, který je mimo provoz nebo mu chybí místo, se přeskočí;
- necommitnutá změna putuje a výsledek se vrací jako staged a necommitnutý, bez čehokoli, co by na hostiteli zůstalo, dokonce ani ve sdíleném adresáři sestavení, který checkout deklaruje: adresář sestavení deriveu je připnut ke svému běhovému adresáři i cíli;
- probíhající sloučení si ponechává oba rodiče;
- nesledované soubory, nevyřešený merge, derive, který maže, a selhaný derive se odmítají každý zvlášť;
- souběžný build do sdíleného adresáře nenahradí spustitelný soubor derive.
Každá z těchto mutací shodí test: zámek vzatý bez ohledu na pozici, ponechané mrtvé tikety, CARGO_TARGET_DIR ukázané na sdílený adresář a CARGO_BUILD_BUILD_DIR ponechané deklaraci checkoutu (2026-10-18 to nechalo 87 GB v build-shared/majordomus na lundře, mimo to, co odstraňují pasti běhu, a svazek na 0 bajtech; I2882).