今天的Saturn不是沒有進度。
從Day 012的committed baseline往後看,9月13日共有十筆commits。昨天仍在working tree裡的durable-observation wave已經提交,接著又長出typed mutation outcomes、execution service、retained coordinator與workspace-runs durable facade,再用A/B publication、missing package、corrupt package與actual older Station做負向acceptance。
功能數量很多,但Day 013最值得寫的畫面不是「新plugin成功執行」。
而是這個:
舊Station可以正常啟動,generic recovery也還能操作;但三個product plugins因為Station不懂新ABI而全部拒絕載入。Worker、data與durable owners保持原狀,系統既不fallback到另一版handler,也不重新launch。
這看起來像一場失敗,實際上卻是plugin architecture很難得的成功證據。
如果平台缺少required ABI時仍「盡力執行」,durable work可能被一套從未承諾相容的host重新解釋;如果retained A不見就自動換B,exact generation只剩文件上的裝飾;如果看不到launch receipt便再launch一次,recovery就可能製造第二個worker。
Day 013因此不問「plugin能不能跑」,而問:當它不應該跑時,整個系統能不能誠實拒絕,同時保住稍後恢復所需的ownership與evidence?
Saturn目前的committed HEAD是main@fb12d87。Day 012之後的十筆commits全部發生在2026年9月13日,可以分成四段:
最後四筆尤其不是再加更多product功能,而是讓負向矩陣更接近真實部署:
這條時間線也改變了Day 012的證據分界。昨天那批durable-observation changes不再只是dirty candidate,已由42d5360提交,後續interface又演進成durable-outcomes。但current workspace上仍有另一波7個tracked modifications與5個untracked files,主要是abrupt recovery acceptance follow-up;本文不把它算進HEAD。
Saturn的Host刻意只提供generic platform floor:
Execution、workspace run、capture、finish與product recovery state machine則留在independently built plugins。
今天的關鍵ABI是durable-outcomes。它沒有把execution schema塞進Host,而是在generic durable commit/ack之外,提供typed outcome:
commit
├─ committed
├─ conflict
└─ incompatible
ack
├─ acknowledged
├─ conflict
└─ incompatible
舊Station 0.1.50-main.2不提供這份新platform ABI。對依賴它的execution service、coordinator與workspace-runs facade來說,正確結果就是incompatible。
Host不能偷偷用legacy durable call猜出等價behavior,因為typed outcome正是product plugin用來區分「成功提交」、「同identity不同payload」與「stored state/contract不相容」的依據。把三種結果壓回一個generic error,可能改變recovery decision。
這裡的拒絕不是Host知道execution domain;相反地,它只知道自己無法滿足package宣告的required import。Product behavior仍在plugins,platform只誠實承認ABI floor不足。
今天的durable execution path不是一個巨大plugin,而是三層product composition:
workspace-runs durable facade
│
▼
execution-coordinator
│
▼
execution-service
│
▼
generic managed execution runtime
三層共享同一個canonical operation request,但不是共用一份可任意修改的state。
durable-control.submit只接受Host提供的current scope。Caller不能在payload裡自報另一個scope,把work綁到不屬於自己的resource namespace。
Operation ID受限為穩定、短小的ASCII identity;request則正規化成execution-request-v1並計算SHA-256 digest。
Facade先用retention pin coordinator的exact topics與contract descriptors,再以durable outcome原子保存:
runs-lifetime resource intent。這些truth成立後,才retained-call coordinator。
Coordinator不直接spawn process。它再pin execution-service的submit、observe、capture、recover與finish routes,保存自己的pending action、child snapshot與execution-lifetime intent。
它的transient correlation map只追蹤目前這次live reply,不能充當delivery或completion evidence。Station restart後,真正能恢復的是durable pending action,不是記憶體裡曾經有一個callback。
Execution service先retain自己的exact executable,再commit launch intent,最後才呼叫generic managed capability。
因此同一個operation會形成三份不同的durable owner records:
| Owner | Attested namespace | 保留的主要truth |
|---|---|---|
| workspace-runs | facade bundle + scope | product run、coordinator route、runs lifetime |
| coordinator | coordinator bundle + scope | child request、pending action、execution route |
| execution-service | service bundle + scope | launch intent、managed receipt、worker resource |
它們可以有相同request digest,卻有不同namespace、revision與lifetime responsibility。Payload不能只拿著operation字串就跨層接管另一個owner。
這也是為什麼older Station refusal不能只「跳過中間plugin」直接操作worker。三層ownership不是decorator chain,而是三個可獨立settle的durable boundary。
Execution service把launch outcome分成accepted、refused與unknown。
這三種狀態不能合併成「成功/失敗」:
accepted:有receipt證明launch已被supervisor接受;refused:有definitive pre-effect evidence證明沒有launch;unknown:effect可能已發生,但reply、inventory或receipt不足以判定。observe()只能讀receipt與status。Healthy inventory暫時缺少worker、runtime unavailable或reply malformed,都不能直接推論effect未發生,更不能自動重新launch。
只有caller明確送出:
recover {
operation,
request-digest
}
而且digest和原request完全一致時,service才可重送相同的launch-v1 identity與payload。Native controller若已有Completed receipt就replay;Pending/Uncertain保持unknown;retired identity也不能被recover復活。
這和Day 012的output replay是兩件不同的事:
Capture與finish也採同樣紀律。Capture需要terminal worker、matching identity、完整manifest與有界change count;finish還要exact capture digest、再次確認capture、commit finish intent,並確認native response同時是released=true與data_retained=true。
Process terminal、output captured、leases retired與work accepted是不同狀態。目前實作甚至明確保留「work acceptance需要另一層verification」;它沒有把CLI exit 0直接升格成coding task完成。
Committed acceptance會先讓一項operation經新版Station建立三層owners、exact retained routes與managed worker。接著planned shutdown這個compatible Station,再啟動實際的舊版Station 0.1.50-main.2。
舊Station本身可以open同一份state,generic operator recovery也仍存在。它不是整個process立即crash。
但因為缺少durable-outcomes ABI,三個product packages都不能admit:
Old Station
├─ generic state ownership:可用
├─ operator recovery floor:可用
├─ workspace-runs facade:incompatible
├─ execution-coordinator:incompatible
└─ execution-service:incompatible
這段期間,正確的不變量是:
換句話說,舊Station能做的,是保護它理解的generic invariants;它不能假裝理解一份較新的product contract。
這個failure mode比「服務暫時Unavailable」更強。Unavailable通常代表目前沒有ready provider;incompatible則代表Host連required ABI語意都無法建立。兩者若都fallback latest,durable owner就失去exact meaning。
Saturn在older-ABI proof之前,先建立兩層較小的負向矩陣。
Existing owner retain coordinator A與execution A的exact descriptors。發布B後,新operation可以選B;舊operation仍必須走A。兩個workers可以同時存在,但route不能混版。
這延續Day 009的closure invariant,今天的新意是它跨越了三層product ownership,而不只是一個provider的nested call。
若retained coordinator A artifact缺失,或execution A bytes corrupt,系統會反覆refuse。B即使完整、ready、排名更高,也不能接管A owner。
只有exact A bytes repair後,compatible Station才恢復原route,而且不二次launch。
如果missing A時改用B,表面上availability更好,實際上卻把「同一個operation」換成另一套未經owner同意的state machine。旧worker、pending action與capture format未必能被B正確解讀。
Old Station缺新ABI時,不能藉由換一版plugin掩蓋platform floor不足。Package selection與Host ABI compatibility是兩個gate;兩者都要通過,retained call才有意義。
所以refusal不是消極地什麼都不做。它主動保護了以下事實:目前沒有任何被證明相容的execution interpretation可以接管這項work。
當compatible Station再次啟動,它會重新admit exact retained packages、restore三層owner records,並觀察原managed worker/receipt,而不是建立一個看似相同的新operation。
Recovery必須保持:
之後產品層仍要明確走capture與finish。
Capture保存output與private workspace evidence;finish確認capture digest、native release與data retention,再讓execution service、coordinator與facade各自完成lifetime settlement。三把owner locks不是同一瞬間被Host強制清空,而是依各層已提交的terminal evidence釋放。
即使如此,work仍不等於accepted。目前尚未有完整的coding task/attempt/check/review product loop。Compatible recovery只能證明同一execution被接回並安全收尾,不能證明patch已通過測試、review或integration。
這個限制讓文章不會把F1 execution recovery誤寫成F4 development workflow完成。
Saturn current committed HEAD是fb12d87c73cc45cdb94adb23bc01b4106a7aacd0。上述older-Station ABI acceptance屬於HEAD,但主案例是planned replacement。
Current workspace仍有7個tracked unstaged changes與5個untracked files,主要修改test harness、Rust acceptance tests、Moon proof task與recovery-boundaries docs。它沒有新增一套production product runtime。
這批dirty follow-up把同一矩陣擴到更尖銳的情境:
durable:write,驗證first domain commit被拒後owner與exact binding仍可恢復。Local review與ignored artifacts記錄focused tests、planned/abrupt cells與full CI結果,但這些changes尚未commit。Day 013因此只把它們列為follow-up direction,不把abrupt older-ABI case寫成committed acceptance。
這個分界也說明ignored proof、tracked review與Git commit不是同一層證據。Local machine曾跑過,不等於clone HEAD就擁有相同test source;review寫了結果,也不能替未提交patch建立release identity。
本文沒有重跑Saturn tests、CI、staging或native proofs。以下都是repository-recorded evidence。
和Day 013主線最直接相關的紀錄包括:
這些證據支持planned older-ABI refusal與compatible recovery。它們不關閉:
因此Day 013證明的是「拒絕與保留ownership」這條bounded contract,不是完整durable ADE已經完成。
把同一問題放回Unity orchestration,會遇到更高風險的版本差異。
假設一個Build run已經由新版plugins建立:
Unity workflow facade
→ build coordinator
→ Unity execution service
→ Editor / batch-mode worker
中途control plane換成舊版Station。舊Station也許仍能打開state directory、看見worker與workspace,卻不理解新版build outcome ABI、artifact manifest或capture settlement。
這時最糟的「恢復」方式包括:
更安全的contract是:
未來Unity vertical slice仍要回答:
Saturn還沒有完整Unity workflow回答這些問題。但今天得到了一條比「舊版也能勉強跑」更可靠的原則:
Plugin acceptance也包括可預期的拒絕。當Host不懂required ABI,正確行為是保住identity、ownership與evidence,等exact compatible path回來;不是偷換handler,也不是把unknown變成retry。
能跑是一種能力。知道什麼時候絕對不能跑,才是durable plugin architecture的邊界。