Kontext
ADR 0052 rozhodlo o směru: jedna kanonická doménová služba relace/kontinuity v apps/majordomus-cli, vlastnící typovaný stavový automat životního cyklu, identitu, události, čerstvost, perzistenci, zotavení a projekci, přičemž .envrc, shimy hooků poskytovatelů, CLI, MCP, HTTP a Cockpit jsou adaptéry a projekce nad ní. Nesestavilo ji a řeklo proč: cesta v shellu má čtyři tisíce řádků a několik workerů ji současně aktivně mění. Přechod v den, kdy čtyři agenti upravují lib/*.sh, je katastrofa merge, ne architektura.
Otázka, na kterou toto rozhodnutí odpovídá, je tedy užší a je to ta, na kterou je třeba odpovědět první: co přesně je doména, aby následný přechod byl mechanický? Odpověď určují tři věci o dnešních úložištích a všechny jsou měření, ne názory.
Úložiště zaznamenávají šest identit jako tři
Čtěte autory zápisů, ne prózu. lib/common.sh má mj_git_repo_id, což je absolutní cesta sdíleného git adresáře, a mj_repository_id, což je URL vzdáleného repozitáře — nebo, když žádný vzdálený není, řetězec local: následovaný hashem cesty worktree. Obojí se nazývá „repozitář“. mj_record_front_matter zapisuje worktree: jako absolutní cestu; mj_session_close zapisuje worktree_id: jako prvních šestnáct hexadecimálních číslic SHA-256 této cesty. Obojí se nazývá „worktree“. Záznam otevřené epizody nese session_id: s-20260910205542-e2a6 i provider_session: <a provider's own UUID> a pořadí rozlišení lib/common.sh bere druhé jako klíč, pod kterým je uloženo první. Obojí se nazývá „relace“.
Každá z těchto dvojic jsou dvě různé věci nesoucí jedno slovo a cena není hypotetická: čtenář, který porovná repository_id lokálního záznamu s týmž polem sdíleného, najde dva řetězce, které se nemohou rovnat, a usoudí, že záznamy spolu nesouvisejí. Prvním úkolem typovaného modelu je udělat z šesti věcí šest typů, aby překladač odmítl porovnání, které próza odmítnout nemohla.
Epizoda není věc, která se děje uvnitř úlohy
Toto je nález ADR 0052 a model ho musí učinit nereprezentovatelným, ne jen nerozumným. Reprezentace, která způsobila šest dní ticha, je ta, kde přechod životního cyklu epizody čte stav úlohy. Model, ve kterém Episode nese Option<TaskId> a přechodová funkce nemá vůbec žádný parametr úlohy, nemůže vadu vyjádřit: není kam dát if outcome != active, který ji způsobil.
Zavření není právě jednou
mj_session_close bylo zpevněno po incidentu z 2026-09-10 — jedna epizoda, s-20260909152316-024f, se čtyřmi neměnnými záznamy, uzavřená v 21:41:35 a pak znovu v 10:39:02, 10:39:18 a 10:39:31 příštího rána, protože koncová událost poskytovatele se může spustit víckrát. Zpevněním je grep -rl "^session_id: $sid$" nad úložištěm před publikací. Tím odpadá opakování, ale ne souběh: dva procesy, které grepují ve stejný okamžik, oba nic nenajdou a oba publikují, a mj_publish_record je navrženo tak, aby nikdy nekolidovalo, takže druhý uspěje. Sken je kontrola a kontrola bez atomického nárokování je okno time-of-check-to-time-of-use s git status a průchodem ledgerem uvnitř — stovky milisekund široké, v repozitáři, kde poskytovatel spouští duplicitní koncové události s odstupem patnácti sekund a jeden checkout sdílí několik relací.
Rozhodnutí
Typovaná doména relací žije v apps/majordomus-cli/src/session/, je budována aditivně a nic v této změně nemění chování běžícího životního cyklu. Shell si ponechává každý zápis, který má. Doména čte stejná úložiště, vlastní sémantiku a vystavuje dvě schopnosti jen pro čtení. Její zápisová cesta existuje, je testována a je dosažitelná jen z knihovny přes konstruktor, který opt-in bere explicitně; žádná plocha ji nevolá a žádný shellový příkaz přes ni nejde. Přechod je další práce a jeho pořadí je zaznamenáno níže.
Šest identit, šest typů, žádné převody mezi nimi. RepositoryId, CheckoutId, EpisodeId, ProviderSessionId, WorkerId a TaskId jsou odlišné newtypy bez From mezi nimi a bez společného traitu, který by dovolil předat jeden tam, kde se chce jiný. Identity repozitáře a checkoutu nejsou vynalezeny znovu: jsou to crate::repository::git_identity a crate::repository::identity, digesty, kterými tento spustitelný soubor už rozlišuje server, lease a index jednoho checkoutu od druhého, a doména zaznamenává vlastní zápis každého úložiště vedle kanonické hodnoty, místo aby mezi nimi vybírala. ProviderSessionId je zdokumentováno, pojmenováno a typováno jako externí korelační id: je to způsob, jakým hook poskytovatele najde epizodu, kterou otevřel, a nikdy není identitou epizody, protože poskytovatel, který žádnou identitu relace neposílá, epizodu přesto má a dva poskytovatelé mohou své relace pojmenovat stejně.
Episode a Task jsou oddělené agregáty. Epizoda nese Option<TaskId> a nic jiného z úlohy. Episode::may_move_to bere cílový stav a hodiny; nemá parametr úlohy, a přechod tak nelze podmínit stavem úlohy. Výsledek úlohy může změnit, co artefakt epizody říká; v tomto modelu není výraz, jímž by rozhodoval, zda se zapíše.
Životní cyklus je typovaný stavový automat se šesti přechody a jedním koncovým stavem. open je Opening → Open; resume je Detached → Open a startovní událost poskytovatele, která dorazí k epizodě, jež je stále otevřená, je tentýž přechod viděný z Open, který automat dovoluje bez přesunu; checkpoint je Open → Open, přechod, který stav nepřesouvá a říká to, místo aby chyběl, protože událost modelovanou jako „nic se nestalo“ nelze spočítat a její počítání je způsob, jak se zastavený zapisovatel stane viditelným; detach je Open → Detached; close je Open → Closed; a recover je Detached → Closed, uskutečněný, když je poslední známka života epizody starší než práh uvíznutí. Closed je koncový: EpisodeState::may_move_to odpoví false na každý přesun z něj, což je tvrzení, které úložiště činí před zápisem, nikoli po něm.
Uzavření proběhne právě jednou a záruku dává primitivum souborového systému, ne prohledávání. Dříve než se složí jakýkoli záznam, closer vytvoří state/sessions-closing/<episode id>.claim pomocí O_EXCL. Právě jeden volající — v tomto procesu, v jiném procesu, na tomto stroji — ten soubor vytvoří; každý další jej pozoruje a vrátí CloseOutcome::AlreadyClosing nebo, existuje-li už záznam, CloseOutcome::AlreadyClosed jmenující záznam, který již stojí. Nárok se bere před nákladnou částí (git status, průchod ledgerem, skládání záznamu), takže okno, které prohledávání v shellu nechává otevřené, neexistuje. Duplicitní uzavření není chyba — druhá koncová událost poskytovatele je běžné chování tohoto poskytovatele — je to zřetelný, diagnostikovatelný výsledek a CloseOutcome má variantu pro případ, kdy historický duplikát už existuje, aby volající mohl hlásit existující škodu repozitáře, místo aby k ní přidával.
Typované události jdou do stávajícího ledgeru; druhé úložiště neexistuje. EventName validuje proti share/events.yaml, což je slovník, proti kterému mj_ledger_append už odmítá neregistrované jméno, a kontrola povinných polí zrcadlí requires: téhož souboru. Připojení jsou zápisy O_APPEND pod advisory flock na souboru ledgeru, což zajistí, že souběžné připojení z jiného procesu přistane celé. Čtení je po řádcích a ztrátové dle deklarace: řádek, který se nenaparsuje, se spočítá a pojmenuje, nikdy se tiše nezahodí a nikdy nedovolí, aby čtení selhalo, protože ledger s jedním useknutým řádkem je stále záznamem všeho ostatního, co se stalo.
Uzamčený append-only JSONL byl vyhodnocen a stačí; SQLite je odmítnuto. Požadavkem je souběžně bezpečné připojování, detekce poškození a zotavení — ne dotazy, ne transakce napříč záznamy, ne indexy. O_APPEND dává atomicitu zápisů pod PIPE_BUF na každé platformě, na které tento nástroj běží, a flock pokrývá zbytek; poškození se zjišťuje po řádcích a zotavuje po řádcích, protože formát nemá žádný stav napříč řádky, který by šel ztratit. Databáze by přidala binární soubor do úložiště, jehož celá hodnota je v tom, že jej člověk může tail, druhou věc k migraci při přechodu, model zamykání, který nepřežije síťový souborový systém o nic lépe než flock, a schéma, které by se okamžitě stalo druhým účtem share/events.yaml. Účetní kniha zůstává textem.
Modul schopnosti je session_domain, ne session. Registr skládá vestavěné moduly a deklarativní druhy do jednoho jmenného prostoru a session už je druh — uzavřené záznamy epizod pod .ai/repo/sessions/. Deklarace modulu tohoto jména způsobila, že se registr vůbec nesestavil, s jednou diagnostikou module 'session' is composed twice na každý záznam v úložišti. Kolizi stojí za to zaznamenat, ne ji potichu obejít přejmenováním: id modulu není soukromé pro executable a ten, kdo příště přidá modul nad existující druh, má tuto větu potkat dřív než dvanáct diagnostik. Id schopností nesou jméno modulu (session_domain.machine, session_domain.identity); jména nástrojů MCP a routy jsou krátká, protože jméno nástroje je projekce, ne identita.
Čerstvost se spotřebovává, neopakuje. fresh | aging | stale | unknown | invalid, Thresholds::judge a epoch_seconds přicházejí s ADR 0052 v capability/builtin/continuity.rs. Tato doména nedefinuje druhý slovník věku. Bere věky v sekundách a odpovídá na jediný predikát, který stavový automat potřebuje — Episode::stranded_after — a klasifikace zůstává tam, kam ji dalo ADR 0052. Jediný kousek časové aritmetiky, který tato změna přidává, je crate::peers::epoch_seconds, přesný inverz formátovače rfc3339, který tam už žije, umístěný vedle něj, aby dvojice byla jednou věcí; až ADR 0052 přistane, kopie continuity.rs se ve prospěch něj smaže.
Zamítnuté alternativy
Přechod hned teď. Cesta shellu je pod současnou úpravou čtyřmi pracovníky ve čtyřech worktree. Přepsání jejích zápisů dnes vyrobí merge, jehož konflikty jsou sémantické, ne textové, v jediném subsystému, kde je tiše chybný merge neviditelný, dokud někdo nepokračuje ze záznamu, který není jeho. Doména se staví nejdřív, vedle shellu, a shell se na ni přesune, až bude v klidu.
Druhé úložiště epizod. Zamítnuto ADR 0052 předem a znovu zde. Ledger a adresáře záznamů jsou substrát. Druhé úložiště je druhý účet událostí, které drží první, což je třída vad, kterou tento celý subsystém stále produkuje.
Znovupoužít tvar ExecutionId pro EpisodeId. Id vrstvy jsou x-20260908T010203Z-0a1b2c3d pro provedení a s-20260910205542-e2a6 pro epizodu — různé tvary razítka a různé délky přípony, protože druhé zapisuje shell nástroj a dělá to měsíce. Doména parsuje to, co je zapsáno, ne to, co by bylo úhlednější; parser, který by odmítl id v .ai/repo/sessions/, by byl parser, který tento repozitář nepřečte.
Přidání třetího výpisu epizod. episodes.* (epizoda spojení MCP, ADR 0043) a lifecycle.* (pozorovatelnost relací) přidávají ve stejném týdnu dva jiní pracovníci nad týmiž úložišti. Třetí projekce téhož seznamu z modulu napsaného proto, aby tento subsystém nezdvojoval sám sebe, by byl vtip, který vypráví sám sebe. Dvě zdejší schopnosti jsou ty, které nemá ani jedna z těch: stroj jako data a identity s jejich zápisy. Přesměrování stávajících projekcí na tuto doménu je krok přechodu.
Učinit z cesty zápisu schopnost Command. Každá projekce registru je jen pro čtení a CapabilityKind::Command je zdokumentováno jako efekt na paměť tohoto procesu a nikde jinde. Schopnost, která by zapisovala záznam relace, by se nabízela přes HTTP volajícímu, který checkout nesdílí. Cesta zápisu je API knihovny a při přechodu se stane příkazem příkazové řádky s LocalReason::WritesRepository.
Důsledky
Crate získá veřejný modul, a tedy nové závazky vůči majordomus quality: dokumentaci, spustitelné příklady a pokrytí modulu. Každá zde přidaná veřejná položka nese doctest.
peers::epoch_seconds a continuity::epoch_seconds budou existovat oba, dokud jsou branch ADR 0052 i tato nesloučeny. Je to známý, textový konflikt o jednom hunku a je zde zaznamenán, aby ten, kdo mergeuje jako druhý, smazal kopii kontinuity, místo aby jednu z nich přejmenoval.
Přechod, v pořadí. Každý krok lze nezávisle nasadit i nezávisle vrátit zpět.
majordomus session close(shell) deleguje na uzavírač domény. To je krok s měřitelným přínosem — závod se uzavře — a nejmenší plochou.majordomus session startdeleguje na otvírač domény; shell si ponechává parsování argumentů a lidský výstup.- Shimy hooků poskytovatelů volají doménu přímo místo
capture session, takže události potvrzení z ADR 0052 zapisuje ten, kdo ví, zda práce poté skončila. checkpointahandoverse zaznamenávají k epizodě místo k úloze, což je vlastní změna ADR 0052 a už je na své větvi; v tom okamžiku selib/checkpoint.shalib/handover.shshellu stanou parsováním argumentů a vykreslováním.- Strana čtení —
continuity.state,lifecycle.*,episodes.*a Cockpit — je přepojena na agregáty domény a duplicitní řešení vcontinuity.rsamj_resolve_latestje smazáno. - Stavový automat
lib/session.sh, jeho resolver a aritmetika čerstvosti se odstraňují. Po tomto kroku nedrží doménový stav nic vlib/.