Přeskočit na obsah

Tato stránka byla automaticky přeložena z angličtiny, jediného jazyka, ve kterém se tento web píše. Číst originál.

Doména relací je typovaná, její identity nejsou zaměnitelné a uzavření proběhne právě jednou

Doména relací je typovaná, její identity nejsou zaměnitelné a uzavření proběhne právě jednou

majordomus://adr/adr-0047navrženo relacecontinuityarchitekturaidentityrust

odkazuje na

  • put_in_force apps/majordomus-cli/src/capability/builtin/session_domain.rs (soubor)
  • put_in_force apps/majordomus-cli/src/session/mod.rs (soubor)
  • put_in_force share/events.yaml (soubor)
  • put_in_force adr-0004
  • put_in_force adr-0014
  • put_in_force adr-0052
  • put_in_force test/cases/240_the_session_domain_is_typed.sh (test)

odkazuje na něj

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.

  1. 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.
  2. majordomus session start deleguje na otvírač domény; shell si ponechává parsování argumentů a lidský výstup.
  3. 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.
  4. checkpoint a handover se 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 se lib/checkpoint.sh a lib/handover.sh shellu stanou parsováním argumentů a vykreslováním.
  5. Strana čtení — continuity.state, lifecycle.*, episodes.* a Cockpit — je přepojena na agregáty domény a duplicitní řešení v continuity.rs a mj_resolve_latest je smazáno.
  6. Stavový automat lib/session.sh, jeho resolver a aritmetika čerstvosti se odstraňují. Po tomto kroku nedrží doménový stav nic v lib/.

Odkud to pochází

kanonický soubor
.ai/repo/adrs/0047-the-session-domain-is-typed-and-its-identities-are-not-interchangeable.md
identity
adr-0047
dostupné také jako
Zdroj MCP majordomus://adr/adr-0047 · majordomus entity show adr/adr-0047 · GET /api/v1/entity?uri=majordomus://adr/adr-0047
v Cockpitu
/cockpit/objects/adr/adr-0047

Tato stránka je generována z výše uvedeného souboru pomocí scripts/generate-site-data; úprava stránky nic nemění, úprava souboru změní všechna rozhraní. Výše uvedené vztahy jsou hrany docs/generated/graph.json, které spustitelný program zapisuje ze stejného indexu.

Prohledává názvy a popisy všech stránek, které tento web publikuje.